Repository: open-telemetry/opentelemetry-python-contrib

Issue:
instrumentation-aiohttp-client: Number of created spans per request increases by 1 with every new request
Latest release 0.32b0 introduced a bug where an additional span is created every time a new aiohttp ClientSession is created. This leads to 1 span per request when the first session is created, 2 spans when the second session gets created, ... and so on.

When `AioHttpClientInstrumentor.instrument()` the instrumentor intercepts the `ClientSession.__init__` call and injects a TraceConfig instance into the parameters that are passed to the ClientSession. With the latest release an additional parameter `trace_configs` was added to the `AioHttpClientInstrumentor.instrument()` method.
The problem is that the `trace_configs` argument is now injected into every newly created ClientSession but it is also shared between all ClientSessions. 
Every time a new ClientSession is created the instrumented `__init__` call now adds one additional trace_config to the shared `trace_configs` argument (associated code can be found [here](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/v0.32b0/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py#L281-L282)). Since one trace_config defines the hooks to create and end one span, every added trace_config will produce an additional span per request.

**Steps to reproduce**
Instrument the aiohttp-client with `AioHttpClientInstrumentor.instrument()`.  Create at least two ClientSessions and make one HTTP request to some arbitrary URL. The produced spans increase with every new ClientSession that is created.

Describe exactly how to reproduce the error. Include a code sample if applicable.

**What is the expected behavior?**
One span per HTTP request independent of the number of ClientSessions that are created.

**What is the actual behavior?**
Number of created spans increase with each newly created ClientSession.



Discussion:
Thinking about it some more i don't quite see the purpose of the newly added `trace_configs` parameter in `AioHttpClientInstrumentor.instrument()`. 
A `TraceConfig` is the mechanism that aiohttp provides to trace requests by registering callbacks for all kind of events (start, end, exception, ...). This is what the instrumentor already uses to create and end spans accordingly. From an OpenTelemetry point of view adding further TraceConfigs would just mean creating more spans. So imo the `trace_configs` parameter to externally add additional TraceConfigs doesn't make much sense.
Same problem with 0.33b0 release
@mariojonke if the last change is not necessary feel free to send a pull request with fix. 

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/src/opentelemetry/instrumentation/aiohttp_client/__init__.py
@@ -262,7 +262,9 @@ def _instrument(
     url_filter: _UrlFilterT = None,
     request_hook: _RequestHookT = None,
     response_hook: _ResponseHookT = None,
-    trace_configs: typing.Optional[aiohttp.TraceConfig] = None,
+    trace_configs: typing.Optional[
+        typing.Sequence[aiohttp.TraceConfig]
+    ] = None,
 ):
     """Enables tracing of all ClientSessions
 
@@ -270,16 +272,15 @@ def _instrument(
     the session's trace_configs.
     """
 
-    if trace_configs is None:
-        trace_configs = []
+    trace_configs = trace_configs or ()
 
     # pylint:disable=unused-argument
     def instrumented_init(wrapped, instance, args, kwargs):
         if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
             return wrapped(*args, **kwargs)
 
-        if kwargs.get("trace_configs"):
-            trace_configs.extend(kwargs.get("trace_configs"))
+        client_trace_configs = list(kwargs.get("trace_configs", ()))
+        client_trace_configs.extend(trace_configs)
 
         trace_config = create_trace_config(
             url_filter=url_filter,
@@ -288,9 +289,9 @@ def instrumented_init(wrapped, instance, args, kwargs):
             tracer_provider=tracer_provider,
         )
         trace_config._is_instrumented_by_opentelemetry = True
-        trace_configs.append(trace_config)
+        client_trace_configs.append(trace_config)
 
-        kwargs["trace_configs"] = trace_configs
+        kwargs["trace_configs"] = client_trace_configs
         return wrapped(*args, **kwargs)
 
     wrapt.wrap_function_wrapper(


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py
--- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py
+++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py
@@ -387,18 +387,35 @@ def test_instrument(self):
         self.assertEqual(200, span.attributes[SpanAttributes.HTTP_STATUS_CODE])
 
     def test_instrument_with_custom_trace_config(self):
+        trace_config = aiohttp.TraceConfig()
+
         AioHttpClientInstrumentor().uninstrument()
-        AioHttpClientInstrumentor().instrument(
-            trace_configs=[aiohttp_client.create_trace_config()]
-        )
+        AioHttpClientInstrumentor().instrument(trace_configs=[trace_config])
 
-        self.assert_spans(0)
+        async def make_request(server: aiohttp.test_utils.TestServer):
+            async with aiohttp.test_utils.TestClient(server) as client:
+                trace_configs = client.session._trace_configs
+                self.assertEqual(2, len(trace_configs))
+                self.assertTrue(trace_config in trace_configs)
+                async with client as session:
+                    await session.get(TestAioHttpClientInstrumentor.URL)
 
-        run_with_test_server(
-            self.get_default_request(), self.URL, self.default_handler
-        )
+        run_with_test_server(make_request, self.URL, self.default_handler)
+        self.assert_spans(1)
+
+    def test_every_request_by_new_session_creates_one_span(self):
+        async def make_request(server: aiohttp.test_utils.TestServer):
+            async with aiohttp.test_utils.TestClient(server) as client:
+                async with client as session:
+                    await session.get(TestAioHttpClientInstrumentor.URL)
 
-        self.assert_spans(2)
+        for request_no in range(3):
+            self.memory_exporter.clear()
+            with self.subTest(request_no=request_no):
+                run_with_test_server(
+                    make_request, self.URL, self.default_handler
+                )
+                self.assert_spans(1)
 
     def test_instrument_with_existing_trace_config(self):
         trace_config = aiohttp.TraceConfig()
@@ -446,7 +463,7 @@ async def uninstrument_request(server: aiohttp.test_utils.TestServer):
         run_with_test_server(
             self.get_default_request(), self.URL, self.default_handler
         )
-        self.assert_spans(2)
+        self.assert_spans(1)
 
     def test_suppress_instrumentation(self):
         token = context.attach(