diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ff8a0a820..f9f790222a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e68e8d736b..db99ed5ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs-requirements.txt b/docs-requirements.txt index dc1cbc7790..cbfd327318 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -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 diff --git a/docs/instrumentation/falcon/falcon.rst b/docs/instrumentation/falcon/falcon.rst new file mode 100644 index 0000000000..327336a7ba --- /dev/null +++ b/docs/instrumentation/falcon/falcon.rst @@ -0,0 +1,7 @@ +OpenTelemetry Falcon Instrumentation +==================================== + +.. automodule:: opentelemetry.instrumentation.falcon + :members: + :undoc-members: + :show-inheritance: diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index 41db811287..159dcaba36 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py @@ -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 ----- @@ -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 @@ -83,7 +130,7 @@ 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 @@ -91,13 +138,17 @@ def _uninstrument(self, **kwargs): 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 @@ -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 @@ -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 diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index 1b68c3d075..190e389b84 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -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( @@ -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") @@ -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" + )