Skip to content

Commit

Permalink
Add ability to exclude some routes in fastapi and starlette (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
srikanthccv committed Dec 8, 2020
1 parent b310ec1 commit 76fda25
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions instrumentation/opentelemetry-instrumentation-fastapi/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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


Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down
15 changes: 15 additions & 0 deletions instrumentation/opentelemetry-instrumentation-starlette/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
# limitations under the License.

import unittest
from unittest.mock import patch

from starlette import applications
from starlette.responses import PlainTextResponse
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


Expand All @@ -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()
Expand All @@ -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

Expand Down

0 comments on commit 76fda25

Please sign in to comment.