Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
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