Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Instantiating AsyncPGInstrumentor after .instrument has been called causes tracing to fail
**Describe your environment**

Python 3.11 on a M2 Mac.

**Steps to reproduce**

Run the following code (with `python -m asyncio` to allow top-level `async`/`await`):
```
import asyncpg

from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.trace import set_tracer_provider

provider = TracerProvider()
processor = BatchSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(processor)
set_tracer_provider(provider)

dsn = "postgres://postgres:password@localhost:54320/postgres"

AsyncPGInstrumentor().instrument()
AsyncPGInstrumentor()

connection = await asyncpg.connect(dsn)
await connection.execute("SELECT 1")
```

**What is the expected behavior?**

The SQL query runs successfully and a span is exported to the console.

**What is the actual behavior?**
What did you see instead?

```
AttributeError: 'NoneType' object has no attribute 'start_as_current_span'
```

**Additional context**

Each instantiation of `AsyncPGInstrumentor` runs `__init__`, which sets `self._tracer` to `None`. However, `BaseInstrumentor` overrides `__new__` to implement the singleton pattern, so only one instance of `AsyncPGInstrumentor` is ever created. Instantiating `AsyncPGInstrumentor` after `instrument` has been called (which sets `self._tracer`) therefore sets `self._tracer` back to `None`, which is a state inconsistent with `_is_instrumented_by_opentelemetry` (which will still be `True`).

A simple solution is to remove the line `self._tracer = None`.


Discussion:

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-asyncpg/src/opentelemetry/instrumentation/asyncpg/__init__.py
@@ -96,11 +96,13 @@ def _hydrate_span_from_args(connection, query, parameters) -> dict:
 
 
 class AsyncPGInstrumentor(BaseInstrumentor):
+
+    _leading_comment_remover = re.compile(r"^/\*.*?\*/")
+    _tracer = None
+
     def __init__(self, capture_parameters=False):
         super().__init__()
         self.capture_parameters = capture_parameters
-        self._tracer = None
-        self._leading_comment_remover = re.compile(r"^/\*.*?\*/")
 
     def instrumentation_dependencies(self) -> Collection[str]:
         return _instruments


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py
--- a/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py
+++ b/instrumentation/opentelemetry-instrumentation-asyncpg/tests/test_asyncpg_wrapper.py
@@ -5,7 +5,7 @@
 
 
 class TestAsyncPGInstrumentation(TestBase):
-    def test_duplicated_instrumentation(self):
+    def test_duplicated_instrumentation_can_be_uninstrumented(self):
         AsyncPGInstrumentor().instrument()
         AsyncPGInstrumentor().instrument()
         AsyncPGInstrumentor().instrument()
@@ -16,6 +16,14 @@ def test_duplicated_instrumentation(self):
                 hasattr(method, "_opentelemetry_ext_asyncpg_applied")
             )
 
+    def test_duplicated_instrumentation_works(self):
+        first = AsyncPGInstrumentor()
+        first.instrument()
+        second = AsyncPGInstrumentor()
+        second.instrument()
+        self.assertIsNotNone(first._tracer)
+        self.assertIsNotNone(second._tracer)
+
     def test_duplicated_uninstrumentation(self):
         AsyncPGInstrumentor().instrument()
         AsyncPGInstrumentor().uninstrument()