Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Missing Falcon instrumentation docs
The docs link on https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-falcon/README.rst results in a HTTP 404.

I'm not sure where these are generated.


Discussion:
This issue was marked stale due to lack of activity. It will be closed in 30 days.

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py
--- 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 query_string and uri_template 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,7 @@ def process_response(
         self, req, resp, resource, req_succeeded=None
     ):  # pylint:disable=R0201
         span = req.env.get(_ENVIRON_SPAN_KEY)
+
         if not span or not span.is_recording():
             return
 
@@ -209,3 +272,6 @@ def process_response(
                     description=reason,
                 )
             )
+
+        if self._response_hook:
+            self._response_hook(span, req, resp)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- 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', 'docs-requirements.txt') }}
       - name: run tox
         run: tox -e ${{ matrix.tox-environment }}
diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py
--- 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")
 
@@ -204,3 +209,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"
+        )