Skip to content

Commit

Permalink
Request/response hooks for Falcon
Browse files Browse the repository at this point in the history
  • Loading branch information
owais committed Apr 6, 2021
1 parent 634c2ac commit 35232e0
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ jobs:
uses: actions/cache@v2
with:
path: .tox
key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}
key: tox-cache-${{ matrix.tox-environment }}-${{ hashFiles('tox.ini', 'dev-requirements.txt') }}-${{ hashFiles('tox.ini', 'docs-requirements.txt') }}
- name: run tox
run: tox -e ${{ matrix.tox-environment }}
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- `opentelemetry-instrumentation-urllib3` Add urllib3 instrumentation
([#299](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/299))
- `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks.
([#415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/415))

## [0.19b0](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.19b0) - 2021-03-26

Expand Down
1 change: 1 addition & 0 deletions docs-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ boto~=2.0
botocore~=1.0
celery>=4.0
flask~=1.0
falcon~=2.0
grpcio~=1.27
mysql-connector-python~=8.0
pymongo~=3.1
Expand Down
7 changes: 7 additions & 0 deletions docs/instrumentation/falcon/falcon.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
OpenTelemetry Falcon Instrumentation
====================================

.. automodule:: opentelemetry.instrumentation.falcon
:members:
:undoc-members:
:show-inheritance:
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@
* The ``falcon.resource`` Span attribute is set so the matched resource.
* Error from Falcon resources are properly caught and recorded.
Configuration
-------------
Exclude lists
*************
To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_FALCON_EXCLUDED_URLS`` with comma delimited regexes representing which URLs to exclude.
For example,
::
export OTEL_PYTHON_FALCON_EXCLUDED_URLS="client/.*/info,healthcheck"
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
Request attributes
********************
To extract certain attributes from Falcon's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS`` to a comma
delimited list of request attribute names.
For example,
::
export OTEL_PYTHON_FALCON_TRACED_REQUEST_ATTRS='query_string,uri_template'
will extract path_info and content_type attributes from every traced request and add them as span attritbues.
Falcon Request object reference: https://falcon.readthedocs.io/en/stable/api/request_and_response.html#id1
Usage
-----
Expand All @@ -39,10 +69,27 @@ def on_get(self, req, resp):
app.add_route('/hello', HelloWorldResource())
Request and Response hooks
***************************
The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a Span is created for a request
and right before the span is finished while processing a response. The hooks can be configured as follows:
::
def request_hook(span, req):
pass
def response_hook(span, req, resp):
pass
FalconInstrumentation().instrument(request_hook=request_hook, response_hook=response_hook)
API
---
"""

from functools import partial
from logging import getLogger
from sys import exc_info

Expand Down Expand Up @@ -83,21 +130,25 @@ class FalconInstrumentor(BaseInstrumentor):

def _instrument(self, **kwargs):
self._original_falcon_api = falcon.API
falcon.API = _InstrumentedFalconAPI
falcon.API = partial(_InstrumentedFalconAPI, **kwargs)

def _uninstrument(self, **kwargs):
falcon.API = self._original_falcon_api


class _InstrumentedFalconAPI(falcon.API):
def __init__(self, *args, **kwargs):
# inject trace middleware
middlewares = kwargs.pop("middleware", [])
if not isinstance(middlewares, (list, tuple)):
middlewares = [middlewares]

self._tracer = trace.get_tracer(__name__, __version__)
trace_middleware = _TraceMiddleware(
self._tracer, kwargs.get("traced_request_attributes")
self._tracer,
kwargs.pop("traced_request_attributes", None),
kwargs.pop("request_hook", None),
kwargs.pop("response_hook", None),
)
middlewares.insert(0, trace_middleware)
kwargs["middleware"] = middlewares
Expand Down Expand Up @@ -148,12 +199,23 @@ def _start_response(status, response_headers, *args, **kwargs):
class _TraceMiddleware:
# pylint:disable=R0201,W0613

def __init__(self, tracer=None, traced_request_attrs=None):
def __init__(
self,
tracer=None,
traced_request_attrs=None,
request_hook=None,
response_hook=None,
):
self.tracer = tracer
self._traced_request_attrs = _traced_request_attrs
self._request_hook = request_hook
self._response_hook = response_hook

def process_request(self, req, resp):
span = req.env.get(_ENVIRON_SPAN_KEY)
if span and self._request_hook:
self._request_hook(span, req)

if not span or not span.is_recording():
return

Expand All @@ -178,6 +240,9 @@ def process_response(
self, req, resp, resource, req_succeeded=None
): # pylint:disable=R0201
span = req.env.get(_ENVIRON_SPAN_KEY)
if span and self._response_hook:
self._response_hook(span, req, resp)

if not span or not span.is_recording():
return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@
from .app import make_app


class TestFalconInstrumentation(TestBase):
class TestFalconBase(TestBase):
def setUp(self):
super().setUp()
FalconInstrumentor().instrument()
FalconInstrumentor().instrument(
request_hook=getattr(self, "request_hook", None),
response_hook=getattr(self, "response_hook", None),
)
self.app = make_app()
# pylint: disable=protected-access
self.env_patch = patch.dict(
Expand Down Expand Up @@ -64,6 +67,8 @@ def tearDown(self):
self.exclude_patch.stop()
self.traced_patch.stop()


class TestFalconInstrumentation(TestFalconBase):
def test_get(self):
self._test_method("GET")

Expand Down Expand Up @@ -206,3 +211,22 @@ def test_traced_not_recording(self):
self.assertTrue(mock_span.is_recording.called)
self.assertFalse(mock_span.set_attribute.called)
self.assertFalse(mock_span.set_status.called)


class TestFalconInstrumentationHooks(TestFalconBase):
# pylint: disable=no-self-use
def request_hook(self, span, req):
span.set_attribute("request_hook_attr", "value from hook")

def response_hook(self, span, req, resp):
span.update_name("set from hook")

def test_hooks(self):
self.client().simulate_get(path="/hello?q=abc")
span = self.memory_exporter.get_finished_spans()[0]

self.assertEqual(span.name, "set from hook")
self.assertIn("request_hook_attr", span.attributes)
self.assertEqual(
span.attributes["request_hook_attr"], "value from hook"
)

0 comments on commit 35232e0

Please sign in to comment.