Repository: open-telemetry/opentelemetry-python-contrib

Issue:
FalconInstrumentor().uninstrument() method do not uninstrument existing Falcon Apps
Similar to #1256  in FastAPI, falcon uninstrument method do not uninstrument existing falcon instrumented apps, which is not the expected behaviour.



Discussion:
@srikanthccv Please assign this to me.
Hello @srikanthccv 
Looks like there is no method to manually uninstrument an api/app in falcon similar to [`uninstrument_app` in fastapi](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py#L201). Should I implement one, as it will be required to uninstrument existing instances?
To reduce the scope of this issue just add a private helper which can pop the middleware. The `instrument_app`/`uninstrument_app` can be a feature request to address in separate ticket.

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
@@ -194,14 +194,16 @@ def response_hook(span, req, resp):
 
 
 class _InstrumentedFalconAPI(getattr(falcon, _instrument_app)):
+    _instrumented_falcon_apps = set()
+
     def __init__(self, *args, **kwargs):
         otel_opts = kwargs.pop("_otel_opts", {})
 
         # inject trace middleware
-        middlewares = kwargs.pop("middleware", [])
+        self._middlewares_list = kwargs.pop("middleware", [])
         tracer_provider = otel_opts.pop("tracer_provider", None)
-        if not isinstance(middlewares, (list, tuple)):
-            middlewares = [middlewares]
+        if not isinstance(self._middlewares_list, (list, tuple)):
+            self._middlewares_list = [self._middlewares_list]
 
         self._otel_tracer = trace.get_tracer(
             __name__, __version__, tracer_provider
@@ -215,12 +217,18 @@ def __init__(self, *args, **kwargs):
             otel_opts.pop("request_hook", None),
             otel_opts.pop("response_hook", None),
         )
-        middlewares.insert(0, trace_middleware)
-        kwargs["middleware"] = middlewares
+        self._middlewares_list.insert(0, trace_middleware)
+        kwargs["middleware"] = self._middlewares_list
 
         self._otel_excluded_urls = get_excluded_urls("FALCON")
+        self._is_instrumented_by_opentelemetry = True
+        _InstrumentedFalconAPI._instrumented_falcon_apps.add(self)
         super().__init__(*args, **kwargs)
 
+    def __del__(self):
+        if self in _InstrumentedFalconAPI._instrumented_falcon_apps:
+            _InstrumentedFalconAPI._instrumented_falcon_apps.remove(self)
+
     def _handle_exception(
         self, arg1, arg2, arg3, arg4
     ):  # pylint: disable=C0103
@@ -229,6 +237,9 @@ def _handle_exception(
 
         # Translation layer for handling the changed arg position of "ex" in Falcon > 2 vs
         # Falcon < 2
+        if not self._is_instrumented_by_opentelemetry:
+            return super()._handle_exception(arg1, arg2, arg3, arg4)
+
         if _falcon_version == 1:
             ex = arg1
             req = arg2
@@ -253,6 +264,9 @@ def __call__(self, env, start_response):
         if self._otel_excluded_urls.url_disabled(env.get("PATH_INFO", "/")):
             return super().__call__(env, start_response)
 
+        if not self._is_instrumented_by_opentelemetry:
+            return super().__call__(env, start_response)
+
         start_time = time_ns()
 
         span, token = _start_internal_or_server_span(
@@ -414,6 +428,33 @@ class FalconInstrumentor(BaseInstrumentor):
     def instrumentation_dependencies(self) -> Collection[str]:
         return _instruments
 
+    def _remove_instrumented_middleware(self, app):
+        if (
+            hasattr(app, "_is_instrumented_by_opentelemetry")
+            and app._is_instrumented_by_opentelemetry
+        ):
+            if _falcon_version == 3:
+                app._unprepared_middleware = [
+                    x
+                    for x in app._unprepared_middleware
+                    if not isinstance(x, _TraceMiddleware)
+                ]
+                app._middleware = app._prepare_middleware(
+                    app._unprepared_middleware,
+                    independent_middleware=app._independent_middleware,
+                )
+            else:
+                app._middlewares_list = [
+                    x
+                    for x in app._middlewares_list
+                    if not isinstance(x, _TraceMiddleware)
+                ]
+                app._middleware = falcon.api_helpers.prepare_middleware(
+                    app._middlewares_list,
+                    independent_middleware=app._independent_middleware,
+                )
+            app._is_instrumented_by_opentelemetry = False
+
     def _instrument(self, **opts):
         self._original_falcon_api = getattr(falcon, _instrument_app)
 
@@ -425,4 +466,7 @@ def __init__(self, *args, **kwargs):
         setattr(falcon, _instrument_app, FalconAPI)
 
     def _uninstrument(self, **kwargs):
+        for app in _InstrumentedFalconAPI._instrumented_falcon_apps:
+            self._remove_instrumented_middleware(app)
+        _InstrumentedFalconAPI._instrumented_falcon_apps.clear()
         setattr(falcon, _instrument_app, self._original_falcon_api)


Test patch:
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
@@ -242,6 +242,18 @@ def test_traced_not_recording(self):
             self.assertFalse(mock_span.set_attribute.called)
             self.assertFalse(mock_span.set_status.called)
 
+    def test_uninstrument_after_instrument(self):
+        self.client().simulate_get(path="/hello")
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+
+        FalconInstrumentor().uninstrument()
+        self.memory_exporter.clear()
+
+        self.client().simulate_get(path="/hello")
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 0)
+
 
 class TestFalconInstrumentationWithTracerProvider(TestBase):
     def setUp(self):