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): def collect_request_attributes(scope):
"""Collects HTTP request attributes from the ASGI scope and returns a """Collects HTTP request attributes from the ASGI scope and returns a
dictionary to be used as span creation attributes.""" dictionary to be used as span creation attributes."""
server = scope.get("server") or ["0.0.0.0", 80] server_host, port, http_url = get_host_port_url_tuple(scope)
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
query_string = scope.get("query_string") query_string = scope.get("query_string")
if query_string and http_url: if query_string and http_url:
if isinstance(query_string, bytes): if isinstance(query_string, bytes):
@ -105,6 +101,17 @@ def collect_request_attributes(scope):
return result 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): def set_status_code(span, status_code):
"""Adds HTTP response attributes to span using the status_code argument.""" """Adds HTTP response attributes to span using the status_code argument."""
if not span.is_recording(): if not span.is_recording():
@ -152,12 +159,13 @@ class OpenTelemetryMiddleware:
Optional: Defaults to get_default_span_details. 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.app = guarantee_single_callable(app)
self.tracer = trace.get_tracer(__name__, __version__) self.tracer = trace.get_tracer(__name__, __version__)
self.span_details_callback = ( self.span_details_callback = (
span_details_callback or get_default_span_details span_details_callback or get_default_span_details
) )
self.excluded_urls = excluded_urls
async def __call__(self, scope, receive, send): async def __call__(self, scope, receive, send):
"""The ASGI application """The ASGI application
@ -170,6 +178,10 @@ class OpenTelemetryMiddleware:
if scope["type"] not in ("http", "websocket"): if scope["type"] not in ("http", "websocket"):
return await self.app(scope, receive, send) 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)) token = context.attach(propagators.extract(carrier_getter, scope))
span_name, additional_attributes = self.span_details_callback(scope) span_name, additional_attributes = self.span_details_callback(scope)

View File

@ -2,6 +2,9 @@
## Unreleased ## 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 ## Version 0.11b0
Released 2020-07-28 Released 2020-07-28

View File

@ -19,6 +19,21 @@ Installation
pip install opentelemetry-instrumentation-fastapi 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 Usage
----- -----

View File

@ -16,10 +16,13 @@ from typing import Optional
import fastapi import fastapi
from starlette.routing import Match from starlette.routing import Match
from opentelemetry.configuration import Configuration
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.fastapi.version import __version__ # noqa from opentelemetry.instrumentation.fastapi.version import __version__ # noqa
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
_excluded_urls = Configuration()._excluded_urls("fastapi")
class FastAPIInstrumentor(BaseInstrumentor): class FastAPIInstrumentor(BaseInstrumentor):
"""An instrumentor for FastAPI """An instrumentor for FastAPI
@ -36,6 +39,7 @@ class FastAPIInstrumentor(BaseInstrumentor):
if not getattr(app, "is_instrumented_by_opentelemetry", False): if not getattr(app, "is_instrumented_by_opentelemetry", False):
app.add_middleware( app.add_middleware(
OpenTelemetryMiddleware, OpenTelemetryMiddleware,
excluded_urls=_excluded_urls,
span_details_callback=_get_route_details, span_details_callback=_get_route_details,
) )
app.is_instrumented_by_opentelemetry = True app.is_instrumented_by_opentelemetry = True
@ -52,7 +56,9 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.add_middleware( 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. # limitations under the License.
import unittest import unittest
from unittest.mock import patch
import fastapi import fastapi
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.configuration import Configuration
from opentelemetry.test.test_base import TestBase from opentelemetry.test.test_base import TestBase
@ -29,10 +31,26 @@ class TestFastAPIManualInstrumentation(TestBase):
def setUp(self): def setUp(self):
super().setUp() 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._instrumentor = otel_fastapi.FastAPIInstrumentor()
self._app = self._create_app() self._app = self._create_app()
self._client = TestClient(self._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): def test_basic_fastapi_call(self):
self._client.get("/foobar") self._client.get("/foobar")
spans = self.memory_exporter.get_finished_spans() spans = self.memory_exporter.get_finished_spans()
@ -54,6 +72,15 @@ class TestFastAPIManualInstrumentation(TestBase):
# the asgi instrumentation is successfully feeding though. # the asgi instrumentation is successfully feeding though.
self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") 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 @staticmethod
def _create_fastapi_app(): def _create_fastapi_app():
app = fastapi.FastAPI() app = fastapi.FastAPI()
@ -66,6 +93,14 @@ class TestFastAPIManualInstrumentation(TestBase):
async def _(username: str): async def _(username: str):
return {"message": username} 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 return app

View File

@ -1,6 +1,8 @@
# Changelog # Changelog
## Unreleased ## 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 ## Version 0.10b0

View File

@ -19,6 +19,21 @@ Installation
pip install opentelemetry-instrumentation-starlette 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 Usage
----- -----

View File

@ -16,10 +16,13 @@ from typing import Optional
from starlette import applications from starlette import applications
from starlette.routing import Match from starlette.routing import Match
from opentelemetry.configuration import Configuration
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.starlette.version import __version__ # noqa from opentelemetry.instrumentation.starlette.version import __version__ # noqa
_excluded_urls = Configuration()._excluded_urls("starlette")
class StarletteInstrumentor(BaseInstrumentor): class StarletteInstrumentor(BaseInstrumentor):
"""An instrumentor for starlette """An instrumentor for starlette
@ -36,6 +39,7 @@ class StarletteInstrumentor(BaseInstrumentor):
if not getattr(app, "is_instrumented_by_opentelemetry", False): if not getattr(app, "is_instrumented_by_opentelemetry", False):
app.add_middleware( app.add_middleware(
OpenTelemetryMiddleware, OpenTelemetryMiddleware,
excluded_urls=_excluded_urls,
span_details_callback=_get_route_details, span_details_callback=_get_route_details,
) )
app.is_instrumented_by_opentelemetry = True app.is_instrumented_by_opentelemetry = True
@ -52,7 +56,9 @@ class _InstrumentedStarlette(applications.Starlette):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.add_middleware( 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. # limitations under the License.
import unittest import unittest
from unittest.mock import patch
from starlette import applications from starlette import applications
from starlette.responses import PlainTextResponse from starlette.responses import PlainTextResponse
@ -20,6 +21,7 @@ from starlette.routing import Route
from starlette.testclient import TestClient from starlette.testclient import TestClient
import opentelemetry.instrumentation.starlette as otel_starlette import opentelemetry.instrumentation.starlette as otel_starlette
from opentelemetry.configuration import Configuration
from opentelemetry.test.test_base import TestBase from opentelemetry.test.test_base import TestBase
@ -31,10 +33,26 @@ class TestStarletteManualInstrumentation(TestBase):
def setUp(self): def setUp(self):
super().setUp() 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._instrumentor = otel_starlette.StarletteInstrumentor()
self._app = self._create_app() self._app = self._create_app()
self._client = TestClient(self._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): def test_basic_starlette_call(self):
self._client.get("/foobar") self._client.get("/foobar")
spans = self.memory_exporter.get_finished_spans() spans = self.memory_exporter.get_finished_spans()
@ -56,13 +74,26 @@ class TestStarletteManualInstrumentation(TestBase):
# the asgi instrumentation is successfully feeding though. # the asgi instrumentation is successfully feeding though.
self.assertEqual(spans[-1].attributes["http.flavor"], "1.1") 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 @staticmethod
def _create_starlette_app(): def _create_starlette_app():
def home(_): def home(_):
return PlainTextResponse("hi") return PlainTextResponse("hi")
def health(_):
return PlainTextResponse("ok")
app = applications.Starlette( app = applications.Starlette(
routes=[Route("/foobar", home), Route("/user/{username}", home)] routes=[
Route("/foobar", home),
Route("/user/{username}", home),
Route("/healthzz", health),
]
) )
return app return app