Repository: open-telemetry/opentelemetry-python

Issue:
Add some support for programmatic instrumentation
[This comment](https://github.com/open-telemetry/opentelemetry-python/pull/551#issuecomment-608993167) in #551 raised the concern about making it possible to use the instrumentors in a programmatic way too, not only automatically.

Using them programmatically means adding code directly in the application that performs instrumentation, for example, [this](https://github.com/open-telemetry/opentelemetry-python/pull/551/files#diff-906a392e80f621fdbc1ea38af3c41081R19) or [this](https://github.com/open-telemetry/opentelemetry-python/pull/551/files#diff-906a392e80f621fdbc1ea38af3c41081L22).

It is very likely that this programmatic use of the instrumentors needs to be implemented in a framework-specific way, this means that the way we can do this kind of instrumentation is different for Flask and is different too for Django, for example. That means that it is possible that we end up having just special code for each framework as opposed to having a universal mechanism that works for every framework (this would mean that this issue could end up being closed without any fix).

This programmatic instrumentation mechanisms can be more sophisticated, for example, in an OpenTracing-related project [here](https://github.com/opentracing-contrib/python-django/blob/master/README.rst#tracing-individual-requests) it is shown how the programmatic instrumentation mechanism allows the user to select which application functions are to be traced.

In summary, what we want to introduce with this issue, is the capability to perform instrumentation without having to directly write the code that creates the spans but also without using the `opentelemetry-auto-instrumentation` command, but with a different mechanism (probably specifically tailored for each framework (Django, Flask, etc.)) like decorators or maybe Python context managers (the ones that use `with`, to avoid any confusion with the OpenTelemetry concept of context :slightly_smiling_face:).

@mauriciovasquezbernal @codeboten we have just talked about this :+1:

Also, to make things clear, with this approach we would have 3 ways of doing instrumentation:
1. The "normal" way (instantiating the spans directly with OpenTelemetry-provided functions)
2. The automatic way (using the `opentelemetry-auto-instrumentation` command)
3. The programmatic way (using a mechanism as described here to instrument specific frameworks)


Discussion:

Resolving patch:
diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py
--- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py
+++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py
@@ -160,9 +160,9 @@ def __init__(self):
         super().__init__()
         self._original_flask = None
 
-    def _instrument(self):
+    def _instrument(self, **kwargs):
         self._original_flask = flask.Flask
         flask.Flask = _InstrumentedFlask
 
-    def _uninstrument(self):
+    def _uninstrument(self, **kwargs):
         flask.Flask = self._original_flask
diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py
--- a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py
+++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py
@@ -26,22 +26,29 @@
 class BaseInstrumentor(ABC):
     """An ABC for instrumentors"""
 
-    def __init__(self):
-        self._is_instrumented = False
+    _instance = None
+    _is_instrumented = False
+
+    def __new__(cls):
+
+        if cls._instance is None:
+            cls._instance = object.__new__(cls)
+
+        return cls._instance
 
     @abstractmethod
-    def _instrument(self) -> None:
+    def _instrument(self, **kwargs):
         """Instrument"""
 
     @abstractmethod
-    def _uninstrument(self) -> None:
+    def _uninstrument(self, **kwargs):
         """Uninstrument"""
 
-    def instrument(self) -> None:
+    def instrument(self, **kwargs):
         """Instrument"""
 
         if not self._is_instrumented:
-            result = self._instrument()
+            result = self._instrument(**kwargs)
             self._is_instrumented = True
             return result
 
@@ -49,11 +56,11 @@ def instrument(self) -> None:
 
         return None
 
-    def uninstrument(self) -> None:
+    def uninstrument(self, **kwargs):
         """Uninstrument"""
 
         if self._is_instrumented:
-            result = self._uninstrument()
+            result = self._uninstrument(**kwargs)
             self._is_instrumented = False
             return result
 


Test patch:
diff --git a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py
--- a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py
+++ b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py
@@ -20,21 +20,20 @@
 
 
 class TestInstrumentor(TestCase):
-    def test_protect(self):
-        class Instrumentor(BaseInstrumentor):
-            def _instrument(self):
-                return "instrumented"
+    class Instrumentor(BaseInstrumentor):
+        def _instrument(self, **kwargs):
+            return "instrumented"
 
-            def _uninstrument(self):
-                return "uninstrumented"
+        def _uninstrument(self, **kwargs):
+            return "uninstrumented"
 
-        instrumentor = Instrumentor()
+    def test_protect(self):
+        instrumentor = self.Instrumentor()
 
         with self.assertLogs(level=WARNING):
             self.assertIs(instrumentor.uninstrument(), None)
 
         self.assertEqual(instrumentor.instrument(), "instrumented")
-
         with self.assertLogs(level=WARNING):
             self.assertIs(instrumentor.instrument(), None)
 
@@ -42,3 +41,6 @@ def _uninstrument(self):
 
         with self.assertLogs(level=WARNING):
             self.assertIs(instrumentor.uninstrument(), None)
+
+    def test_singleton(self):
+        self.assertIs(self.Instrumentor(), self.Instrumentor())