From 57e182a3f91cf23789c62d0a50073188f0bf303e Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Tue, 6 Jul 2021 14:52:51 +0100
Subject: [PATCH 01/19] test removing thread specific logic. upgrade to
 sqlalchemy ~1.4

---
 .../instrumentation/sqlalchemy/engine.py      | 22 ++++++++++---------
 tox.ini                                       |  4 ++--
 2 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index e69c6dbcb4..6adf48cd26 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from threading import local
+# from threading import local
 
 from sqlalchemy.event import listen  # pylint: disable=no-name-in-module
 
@@ -60,19 +60,19 @@ def __init__(self, tracer, engine):
         self.engine = engine
         self.vendor = _normalize_vendor(engine.name)
         self.cursor_mapping = {}
-        self.local = local()
+        # self.local = local()
 
         listen(engine, "before_cursor_execute", self._before_cur_exec)
         listen(engine, "after_cursor_execute", self._after_cur_exec)
         listen(engine, "handle_error", self._handle_error)
 
-    @property
-    def current_thread_span(self):
-        return getattr(self.local, "current_span", None)
+    # @property
+    # def current_thread_span(self):
+    #     return getattr(self.local, "current_span", None)
 
-    @current_thread_span.setter
-    def current_thread_span(self, span):
-        setattr(self.local, "current_span", span)
+    # @current_thread_span.setter
+    # def current_thread_span(self, span):
+    #     setattr(self.local, "current_span", span)
 
     def _operation_name(self, db_name, statement):
         parts = []
@@ -100,7 +100,8 @@ def _before_cur_exec(self, conn, cursor, statement, *args):
             self._operation_name(db_name, statement),
             kind=trace.SpanKind.CLIENT,
         )
-        self.current_thread_span = self.cursor_mapping[cursor] = span
+        # self.current_thread_span =
+        self.cursor_mapping[cursor] = span
         with trace.use_span(span, end_on_exit=False):
             if span.is_recording():
                 span.set_attribute(SpanAttributes.DB_STATEMENT, statement)
@@ -118,7 +119,8 @@ def _after_cur_exec(self, conn, cursor, statement, *args):
         self._cleanup(cursor)
 
     def _handle_error(self, context):
-        span = self.current_thread_span
+        span = self.cursor_mapping[context.cursor]
+        # span = self.current_thread_span
         if span is None:
             return
 
diff --git a/tox.ini b/tox.ini
index 2d58b8928e..f720b8fd43 100644
--- a/tox.ini
+++ b/tox.ini
@@ -329,7 +329,7 @@ commands =
 
 [testenv:lint]
 basepython: python3.9
-recreate = False 
+recreate = False
 deps =
   -c dev-requirements.txt
   flaky
@@ -399,7 +399,7 @@ deps =
   PyMySQL ~= 0.10.1
   psycopg2 ~= 2.8.4
   aiopg >= 0.13.0, < 1.3.0
-  sqlalchemy ~= 1.3.16
+  sqlalchemy ~= 1.4
   redis ~= 3.3.11
   celery[pytest] >= 4.0, < 6.0
   protobuf>=3.13.0

From 8d58c2b6d09ab89e62ef48a1ce861ebaa0c1b831 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Tue, 6 Jul 2021 15:52:51 +0100
Subject: [PATCH 02/19] wip

---
 .../instrumentation/sqlalchemy/engine.py      | 22 +++++++++----------
 .../tests/sqlalchemy_tests/mixins.py          |  2 ++
 .../tests/sqlalchemy_tests/test_sqlite.py     |  2 +-
 3 files changed, 14 insertions(+), 12 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index 6adf48cd26..ab8f84bd07 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# from threading import local
+from threading import local
 
 from sqlalchemy.event import listen  # pylint: disable=no-name-in-module
 
@@ -60,19 +60,19 @@ def __init__(self, tracer, engine):
         self.engine = engine
         self.vendor = _normalize_vendor(engine.name)
         self.cursor_mapping = {}
-        # self.local = local()
+        self.local = local()
 
         listen(engine, "before_cursor_execute", self._before_cur_exec)
         listen(engine, "after_cursor_execute", self._after_cur_exec)
         listen(engine, "handle_error", self._handle_error)
 
-    # @property
-    # def current_thread_span(self):
-    #     return getattr(self.local, "current_span", None)
+    @property
+    def current_thread_span(self):
+        return getattr(self.local, "current_span", None)
 
-    # @current_thread_span.setter
-    # def current_thread_span(self, span):
-    #     setattr(self.local, "current_span", span)
+    @current_thread_span.setter
+    def current_thread_span(self, span):
+        setattr(self.local, "current_span", span)
 
     def _operation_name(self, db_name, statement):
         parts = []
@@ -100,7 +100,7 @@ def _before_cur_exec(self, conn, cursor, statement, *args):
             self._operation_name(db_name, statement),
             kind=trace.SpanKind.CLIENT,
         )
-        # self.current_thread_span =
+        self.current_thread_span = span
         self.cursor_mapping[cursor] = span
         with trace.use_span(span, end_on_exit=False):
             if span.is_recording():
@@ -119,8 +119,8 @@ def _after_cur_exec(self, conn, cursor, statement, *args):
         self._cleanup(cursor)
 
     def _handle_error(self, context):
-        span = self.cursor_mapping[context.cursor]
-        # span = self.current_thread_span
+        # span = self.cursor_mapping[context.cursor]
+        span = self.current_thread_span
         if span is None:
             return
 
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
index c2e5548ab1..04db609886 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
@@ -15,6 +15,7 @@
 import contextlib
 import logging
 import threading
+import unittest
 
 from sqlalchemy import Column, Integer, String, create_engine, insert
 from sqlalchemy.ext.declarative import declarative_base
@@ -242,4 +243,5 @@ def insert_players(session):
                 close_all_sessions()
 
         spans = self.memory_exporter.get_finished_spans()
+        breakpoint()
         self.assertEqual(len(spans), 5)
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
index 0acba0fec2..981e82d7c2 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
@@ -35,7 +35,7 @@ class SQLiteTestCase(SQLAlchemyTestMixin):
     def test_engine_execute_errors(self):
         # ensures that SQL errors are reported
         stmt = "SELECT * FROM a_wrong_table"
-        with pytest.raises(OperationalError):
+        with pytest.raises(Exception):
             with self.connection() as conn:
                 conn.execute(stmt).fetchall()
 

From 51c53fff7d970478d7b332e27fddebe95db85466 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Tue, 6 Jul 2021 16:13:36 +0100
Subject: [PATCH 03/19] add _span to sqlalchemy execution context instead of
 maintaining mapping

---
 .../instrumentation/sqlalchemy/engine.py      | 32 ++++---------------
 .../tests/sqlalchemy_tests/mixins.py          |  1 -
 .../tests/sqlalchemy_tests/test_sqlite.py     |  2 +-
 3 files changed, 7 insertions(+), 28 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index ab8f84bd07..b1297e9bcb 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -12,8 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from threading import local
-
 from sqlalchemy.event import listen  # pylint: disable=no-name-in-module
 
 from opentelemetry import trace
@@ -59,21 +57,11 @@ def __init__(self, tracer, engine):
         self.tracer = tracer
         self.engine = engine
         self.vendor = _normalize_vendor(engine.name)
-        self.cursor_mapping = {}
-        self.local = local()
 
         listen(engine, "before_cursor_execute", self._before_cur_exec)
         listen(engine, "after_cursor_execute", self._after_cur_exec)
         listen(engine, "handle_error", self._handle_error)
 
-    @property
-    def current_thread_span(self):
-        return getattr(self.local, "current_span", None)
-
-    @current_thread_span.setter
-    def current_thread_span(self, span):
-        setattr(self.local, "current_span", span)
-
     def _operation_name(self, db_name, statement):
         parts = []
         if isinstance(statement, str):
@@ -90,7 +78,7 @@ def _operation_name(self, db_name, statement):
         return " ".join(parts)
 
     # pylint: disable=unused-argument
-    def _before_cur_exec(self, conn, cursor, statement, *args):
+    def _before_cur_exec(self, conn, cursor, statement, params, context, executemany):
         attrs, found = _get_attributes_from_url(conn.engine.url)
         if not found:
             attrs = _get_attributes_from_cursor(self.vendor, cursor, attrs)
@@ -100,8 +88,6 @@ def _before_cur_exec(self, conn, cursor, statement, *args):
             self._operation_name(db_name, statement),
             kind=trace.SpanKind.CLIENT,
         )
-        self.current_thread_span = span
-        self.cursor_mapping[cursor] = span
         with trace.use_span(span, end_on_exit=False):
             if span.is_recording():
                 span.set_attribute(SpanAttributes.DB_STATEMENT, statement)
@@ -109,18 +95,18 @@ def _before_cur_exec(self, conn, cursor, statement, *args):
                 for key, value in attrs.items():
                     span.set_attribute(key, value)
 
+        context._span = span
+
     # pylint: disable=unused-argument
-    def _after_cur_exec(self, conn, cursor, statement, *args):
-        span = self.cursor_mapping.get(cursor, None)
+    def _after_cur_exec(self, conn, cursor, statement, params, context, executemany):
+        span = getattr(context, '_span', None)
         if span is None:
             return
 
         span.end()
-        self._cleanup(cursor)
 
     def _handle_error(self, context):
-        # span = self.cursor_mapping[context.cursor]
-        span = self.current_thread_span
+        span = getattr(context.execution_context, '_span', None)
         if span is None:
             return
 
@@ -131,13 +117,7 @@ def _handle_error(self, context):
                 )
         finally:
             span.end()
-            self._cleanup(context.cursor)
 
-    def _cleanup(self, cursor):
-        try:
-            del self.cursor_mapping[cursor]
-        except KeyError:
-            pass
 
 
 def _get_attributes_from_url(url):
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
index 04db609886..a3bd18ddf7 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
@@ -243,5 +243,4 @@ def insert_players(session):
                 close_all_sessions()
 
         spans = self.memory_exporter.get_finished_spans()
-        breakpoint()
         self.assertEqual(len(spans), 5)
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
index 981e82d7c2..0acba0fec2 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
@@ -35,7 +35,7 @@ class SQLiteTestCase(SQLAlchemyTestMixin):
     def test_engine_execute_errors(self):
         # ensures that SQL errors are reported
         stmt = "SELECT * FROM a_wrong_table"
-        with pytest.raises(Exception):
+        with pytest.raises(OperationalError):
             with self.connection() as conn:
                 conn.execute(stmt).fetchall()
 

From 3d4087c29bb33025efa6fc8a1c0992055ba88885 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Tue, 6 Jul 2021 17:07:26 +0100
Subject: [PATCH 04/19] account for add_all optimisations in sqlalchemy 1.4

---
 .../opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
index a3bd18ddf7..c5fdd925ab 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
@@ -243,4 +243,4 @@ def insert_players(session):
                 close_all_sessions()
 
         spans = self.memory_exporter.get_finished_spans()
-        self.assertEqual(len(spans), 5)
+        self.assertEqual(len(spans), 5 if self.VENDOR not in ["postgresql"] else 3)

From 4a723bbdc037cd62046b9b177efac0c589588677 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Tue, 6 Jul 2021 17:48:11 +0100
Subject: [PATCH 05/19] update docstring

---
 .../instrumentation/sqlalchemy/__init__.py           | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
index 01db312b3f..de85174e36 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
@@ -36,6 +36,18 @@
         engine=engine,
     )
 
+    # of the async variant of SQLAlchemy
+
+    from sqlalchemy.ext.asyncio import create_async_engine
+
+    from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
+    import sqlalchemy
+
+    engine = create_async_engine("sqlite:///:memory:")
+    SQLAlchemyInstrumentor().instrument(
+        engine=engine.sync_engine
+    )
+
 API
 ---
 """

From 5ade34e0322688ce742c2b4432e3a4316135c5fb Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 10:20:46 +0100
Subject: [PATCH 06/19] add tests for async. fix black issues

---
 .../instrumentation/sqlalchemy/engine.py      |  5 ++-
 .../instrumentation/sqlalchemy/package.py     |  2 +-
 .../tests/test_sqlalchemy.py                  | 33 +++++++++++++++++--
 .../tests/sqlalchemy_tests/test_mssql.py      |  3 +-
 .../tests/sqlalchemy_tests/test_mysql.py      |  3 +-
 .../tests/sqlalchemy_tests/test_postgres.py   |  3 +-
 .../tests/sqlalchemy_tests/test_sqlite.py     |  3 +-
 tox.ini                                       |  9 +++--
 8 files changed, 49 insertions(+), 12 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index b1297e9bcb..a924da7276 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -113,7 +113,10 @@ def _handle_error(self, context):
         try:
             if span.is_recording():
                 span.set_status(
-                    Status(StatusCode.ERROR, str(context.original_exception),)
+                    Status(
+                        StatusCode.ERROR,
+                        str(context.original_exception),
+                    )
                 )
         finally:
             span.end()
diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py
index d608d3476a..c79b14cfd7 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py
@@ -13,4 +13,4 @@
 # limitations under the License.
 
 
-_instruments = ("sqlalchemy",)
+_instruments = ("sqlalchemy >= 1.3",)
diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
index 4a633687e6..84904ad859 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
@@ -11,13 +11,19 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+from typing import Coroutine
 from unittest import mock
 
 from sqlalchemy import create_engine
-
+import sqlalchemy
 from opentelemetry import trace
 from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
 from opentelemetry.test.test_base import TestBase
+import asyncio
+
+
+def _call_async(coro: Coroutine):
+    return asyncio.get_event_loop().run_until_complete(coro)
 
 
 class TestSqlalchemyInstrumentation(TestBase):
@@ -28,7 +34,8 @@ def tearDown(self):
     def test_trace_integration(self):
         engine = create_engine("sqlite:///:memory:")
         SQLAlchemyInstrumentor().instrument(
-            engine=engine, tracer_provider=self.tracer_provider,
+            engine=engine,
+            tracer_provider=self.tracer_provider,
         )
         cnx = engine.connect()
         cnx.execute("SELECT	1 + 1;").fetchall()
@@ -38,6 +45,25 @@ def test_trace_integration(self):
         self.assertEqual(spans[0].name, "SELECT :memory:")
         self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
 
+    def test_async_trace_integration(self):
+        if sqlalchemy.__version__.startswith("1.3"):
+            return
+        from sqlalchemy.ext.asyncio import (
+            create_async_engine,
+        )  # pylint: disable-all
+
+        engine = create_async_engine("sqlite+aiosqlite:///:memory:")
+        SQLAlchemyInstrumentor().instrument(
+            engine=engine.sync_engine, tracer_provider=self.tracer_provider
+        )
+        cnx = _call_async(engine.connect())
+        _call_async(cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))).fetchall()
+        _call_async(cnx.close())
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+        self.assertEqual(spans[0].name, "SELECT :memory:")
+        self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
+
     def test_not_recording(self):
         mock_tracer = mock.Mock()
         mock_span = mock.Mock()
@@ -47,7 +73,8 @@ def test_not_recording(self):
             tracer.return_value = mock_tracer
             engine = create_engine("sqlite:///:memory:")
             SQLAlchemyInstrumentor().instrument(
-                engine=engine, tracer_provider=self.tracer_provider,
+                engine=engine,
+                tracer_provider=self.tracer_provider,
             )
             cnx = engine.connect()
             cnx.execute("SELECT	1 + 1;").fetchall()
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py
index ef9cac051a..c48085a0e3 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py
@@ -85,7 +85,8 @@ def test_engine_execute_errors(self):
         self.assertTrue(span.end_time - span.start_time > 0)
         # check the error
         self.assertIs(
-            span.status.status_code, trace.StatusCode.ERROR,
+            span.status.status_code,
+            trace.StatusCode.ERROR,
         )
         self.assertIn("a_wrong_table", span.status.description)
 
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py
index c9e0a8dd1e..730939fd29 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py
@@ -84,6 +84,7 @@ def test_engine_execute_errors(self):
         self.assertTrue(span.end_time - span.start_time > 0)
         # check the error
         self.assertIs(
-            span.status.status_code, trace.StatusCode.ERROR,
+            span.status.status_code,
+            trace.StatusCode.ERROR,
         )
         self.assertIn("a_wrong_table", span.status.description)
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py
index d72791d970..62c880d140 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py
@@ -78,7 +78,8 @@ def test_engine_execute_errors(self):
         self.assertTrue(span.end_time - span.start_time > 0)
         # check the error
         self.assertIs(
-            span.status.status_code, trace.StatusCode.ERROR,
+            span.status.status_code,
+            trace.StatusCode.ERROR,
         )
         self.assertIn("a_wrong_table", span.status.description)
 
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
index 0acba0fec2..aa33b1ad0d 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
@@ -54,7 +54,8 @@ def test_engine_execute_errors(self):
         self.assertTrue((span.end_time - span.start_time) > 0)
         # check the error
         self.assertIs(
-            span.status.status_code, trace.StatusCode.ERROR,
+            span.status.status_code,
+            trace.StatusCode.ERROR,
         )
         self.assertEqual(
             span.status.description, "no such table: a_wrong_table"
diff --git a/tox.ini b/tox.ini
index f720b8fd43..568346aff3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -122,8 +122,8 @@ envlist =
     py3{6,7,8,9}-test-instrumentation-grpc
 
     ; opentelemetry-instrumentation-sqlalchemy
-    py3{6,7,8,9}-test-instrumentation-sqlalchemy
-    pypy3-test-instrumentation-sqlalchemy
+    py3{6,7,8,9}-test-instrumentation-sqlalchemy{13,14}
+    pypy3-test-instrumentation-sqlalchemy{13,14}
 
     ; opentelemetry-instrumentation-redis
     py3{6,7,8,9}-test-instrumentation-redis
@@ -173,6 +173,9 @@ deps =
   elasticsearch6: elasticsearch>=6.0,<7.0
   elasticsearch7: elasticsearch-dsl>=7.0,<8.0
   elasticsearch7: elasticsearch>=7.0,<8.0
+  sqlalchemy13: sqlalchemy~=1.3,<1.4
+  sqlalchemy14: aiosqlite
+  sqlalchemy14: sqlalchemy~=1.4
 
   ; FIXME: add coverage testing
   ; FIXME: add mypy testing
@@ -290,7 +293,7 @@ commands_pre =
 
   sklearn: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test]
 
-  sqlalchemy: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test]
+  sqlalchemy{13,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test]
 
   elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-python-core/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test]
 

From ee58049ac5aeeaf911bbccd887b607df2e790265 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 10:37:53 +0100
Subject: [PATCH 07/19] lint fixes

---
 .../instrumentation/sqlalchemy/__init__.py    | 10 +++++++
 .../instrumentation/sqlalchemy/engine.py      | 23 ++++++++++++----
 .../tests/test_sqlalchemy.py                  | 26 ++++++++++++++++---
 .../tests/sqlalchemy_tests/mixins.py          |  4 ++-
 4 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
index de85174e36..b5816a38d4 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
@@ -60,6 +60,7 @@
 from opentelemetry.instrumentation.sqlalchemy.engine import (
     EngineTracer,
     _get_tracer,
+    _wrap_create_async_engine,
     _wrap_create_engine,
 )
 from opentelemetry.instrumentation.sqlalchemy.package import _instruments
@@ -88,6 +89,13 @@ def _instrument(self, **kwargs):
         """
         _w("sqlalchemy", "create_engine", _wrap_create_engine)
         _w("sqlalchemy.engine", "create_engine", _wrap_create_engine)
+        if sqlalchemy.__version__.startswith("1.4"):
+            _w(
+                "sqlalchemy.ext.asyncio",
+                "create_async_engine",
+                _wrap_create_async_engine,
+            )
+
         if kwargs.get("engine") is not None:
             return EngineTracer(
                 _get_tracer(
@@ -100,3 +108,5 @@ def _instrument(self, **kwargs):
     def _uninstrument(self, **kwargs):
         unwrap(sqlalchemy, "create_engine")
         unwrap(sqlalchemy.engine, "create_engine")
+        if sqlalchemy.__version__.startswith("1.4"):
+            unwrap(sqlalchemy.ext.asyncio, "create_async_engine")
diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index a924da7276..95b8230b31 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -42,6 +42,16 @@ def _get_tracer(engine, tracer_provider=None):
     )
 
 
+# pylint: disable=unused-argument
+def _wrap_create_async_engine(func, module, args, kwargs):
+    """Trace the SQLAlchemy engine, creating an `EngineTracer`
+    object that will listen to SQLAlchemy events.
+    """
+    engine = func(*args, **kwargs)
+    EngineTracer(_get_tracer(engine), engine.sync_engine)
+    return engine
+
+
 # pylint: disable=unused-argument
 def _wrap_create_engine(func, module, args, kwargs):
     """Trace the SQLAlchemy engine, creating an `EngineTracer`
@@ -78,7 +88,9 @@ def _operation_name(self, db_name, statement):
         return " ".join(parts)
 
     # pylint: disable=unused-argument
-    def _before_cur_exec(self, conn, cursor, statement, params, context, executemany):
+    def _before_cur_exec(
+        self, conn, cursor, statement, params, context, executemany
+    ):
         attrs, found = _get_attributes_from_url(conn.engine.url)
         if not found:
             attrs = _get_attributes_from_cursor(self.vendor, cursor, attrs)
@@ -98,15 +110,17 @@ def _before_cur_exec(self, conn, cursor, statement, params, context, executemany
         context._span = span
 
     # pylint: disable=unused-argument
-    def _after_cur_exec(self, conn, cursor, statement, params, context, executemany):
-        span = getattr(context, '_span', None)
+    def _after_cur_exec(
+        self, conn, cursor, statement, params, context, executemany
+    ):
+        span = getattr(context, "_span", None)
         if span is None:
             return
 
         span.end()
 
     def _handle_error(self, context):
-        span = getattr(context.execution_context, '_span', None)
+        span = getattr(context.execution_context, "_span", None)
         if span is None:
             return
 
@@ -122,7 +136,6 @@ def _handle_error(self, context):
             span.end()
 
 
-
 def _get_attributes_from_url(url):
     """Set connection tags from the url. return true if successful."""
     attrs = {}
diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
index 84904ad859..caa7960211 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
@@ -11,15 +11,16 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+import asyncio
 from typing import Coroutine
 from unittest import mock
 
-from sqlalchemy import create_engine
 import sqlalchemy
+from sqlalchemy import create_engine
+
 from opentelemetry import trace
 from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
 from opentelemetry.test.test_base import TestBase
-import asyncio
 
 
 def _call_async(coro: Coroutine):
@@ -48,9 +49,9 @@ def test_trace_integration(self):
     def test_async_trace_integration(self):
         if sqlalchemy.__version__.startswith("1.3"):
             return
-        from sqlalchemy.ext.asyncio import (
+        from sqlalchemy.ext.asyncio import (  # pylint: disable-all
             create_async_engine,
-        )  # pylint: disable-all
+        )
 
         engine = create_async_engine("sqlite+aiosqlite:///:memory:")
         SQLAlchemyInstrumentor().instrument(
@@ -95,3 +96,20 @@ def test_create_engine_wrapper(self):
         self.assertEqual(len(spans), 1)
         self.assertEqual(spans[0].name, "SELECT :memory:")
         self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
+
+    def test_create_async_engine_wrapper(self):
+        SQLAlchemyInstrumentor().instrument()
+        if sqlalchemy.__version__.startswith("1.3"):
+            return
+        from sqlalchemy.ext.asyncio import (  # pylint: disable-all
+            create_async_engine,
+        )
+
+        engine = create_async_engine("sqlite+aiosqlite:///:memory:")
+        cnx = _call_async(engine.connect())
+        _call_async(cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))).fetchall()
+        _call_async(cnx.close())
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+        self.assertEqual(spans[0].name, "SELECT :memory:")
+        self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
index c5fdd925ab..1493ce44cb 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
@@ -243,4 +243,6 @@ def insert_players(session):
                 close_all_sessions()
 
         spans = self.memory_exporter.get_finished_spans()
-        self.assertEqual(len(spans), 5 if self.VENDOR not in ["postgresql"] else 3)
+        self.assertEqual(
+            len(spans), 5 if self.VENDOR not in ["postgresql"] else 3
+        )

From c034e9f7e317d07714029a1afe1797b60a29353b Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 11:53:36 +0100
Subject: [PATCH 08/19] run tox -e generate

---
 .../instrumentation/sqlalchemy/engine.py      |   5 +-
 .../tests/test_sqlalchemy.py                  |   6 +-
 .../instrumentation/bootstrap_gen.py          | 138 ++++++++++++++++++
 3 files changed, 141 insertions(+), 8 deletions(-)
 create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index 95b8230b31..f04ce33449 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -127,10 +127,7 @@ def _handle_error(self, context):
         try:
             if span.is_recording():
                 span.set_status(
-                    Status(
-                        StatusCode.ERROR,
-                        str(context.original_exception),
-                    )
+                    Status(StatusCode.ERROR, str(context.original_exception),)
                 )
         finally:
             span.end()
diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
index caa7960211..361e48fd09 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
@@ -35,8 +35,7 @@ def tearDown(self):
     def test_trace_integration(self):
         engine = create_engine("sqlite:///:memory:")
         SQLAlchemyInstrumentor().instrument(
-            engine=engine,
-            tracer_provider=self.tracer_provider,
+            engine=engine, tracer_provider=self.tracer_provider,
         )
         cnx = engine.connect()
         cnx.execute("SELECT	1 + 1;").fetchall()
@@ -74,8 +73,7 @@ def test_not_recording(self):
             tracer.return_value = mock_tracer
             engine = create_engine("sqlite:///:memory:")
             SQLAlchemyInstrumentor().instrument(
-                engine=engine,
-                tracer_provider=self.tracer_provider,
+                engine=engine, tracer_provider=self.tracer_provider,
             )
             cnx = engine.connect()
             cnx.execute("SELECT	1 + 1;").fetchall()
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
new file mode 100644
index 0000000000..ccbf116e35
--- /dev/null
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
@@ -0,0 +1,138 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES.
+# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE.
+
+libraries = {
+    "aiohttp": {
+        "library": "aiohttp ~= 3.0",
+        "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.23.dev0",
+    },
+    "aiopg": {
+        "library": "aiopg >= 0.13.0, < 1.3.0",
+        "instrumentation": "opentelemetry-instrumentation-aiopg==0.23.dev0",
+    },
+    "asgiref": {
+        "library": "asgiref ~= 3.0",
+        "instrumentation": "opentelemetry-instrumentation-asgi==0.23.dev0",
+    },
+    "asyncpg": {
+        "library": "asyncpg >= 0.12.0",
+        "instrumentation": "opentelemetry-instrumentation-asyncpg==0.23.dev0",
+    },
+    "boto": {
+        "library": "boto~=2.0",
+        "instrumentation": "opentelemetry-instrumentation-boto==0.23.dev0",
+    },
+    "botocore": {
+        "library": "botocore ~= 1.0",
+        "instrumentation": "opentelemetry-instrumentation-botocore==0.23.dev0",
+    },
+    "celery": {
+        "library": "celery >= 4.0, < 6.0",
+        "instrumentation": "opentelemetry-instrumentation-celery==0.23.dev0",
+    },
+    "django": {
+        "library": "django >= 1.10",
+        "instrumentation": "opentelemetry-instrumentation-django==0.23.dev0",
+    },
+    "elasticsearch": {
+        "library": "elasticsearch >= 2.0",
+        "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.23.dev0",
+    },
+    "falcon": {
+        "library": "falcon ~= 2.0",
+        "instrumentation": "opentelemetry-instrumentation-falcon==0.23.dev0",
+    },
+    "fastapi": {
+        "library": "fastapi ~= 0.58.1",
+        "instrumentation": "opentelemetry-instrumentation-fastapi==0.23.dev0",
+    },
+    "flask": {
+        "library": "flask >= 1.0, < 3.0",
+        "instrumentation": "opentelemetry-instrumentation-flask==0.23.dev0",
+    },
+    "grpcio": {
+        "library": "grpcio ~= 1.27",
+        "instrumentation": "opentelemetry-instrumentation-grpc==0.23.dev0",
+    },
+    "httpx": {
+        "library": "httpx >= 0.18.0, < 0.19.0",
+        "instrumentation": "opentelemetry-instrumentation-httpx==0.23.dev0",
+    },
+    "jinja2": {
+        "library": "jinja2~=2.7",
+        "instrumentation": "opentelemetry-instrumentation-jinja2==0.23.dev0",
+    },
+    "mysql-connector-python": {
+        "library": "mysql-connector-python ~= 8.0",
+        "instrumentation": "opentelemetry-instrumentation-mysql==0.23.dev0",
+    },
+    "psycopg2": {
+        "library": "psycopg2 >= 2.7.3.1",
+        "instrumentation": "opentelemetry-instrumentation-psycopg2==0.23.dev0",
+    },
+    "pymemcache": {
+        "library": "pymemcache ~= 1.3",
+        "instrumentation": "opentelemetry-instrumentation-pymemcache==0.23.dev0",
+    },
+    "pymongo": {
+        "library": "pymongo ~= 3.1",
+        "instrumentation": "opentelemetry-instrumentation-pymongo==0.23.dev0",
+    },
+    "PyMySQL": {
+        "library": "PyMySQL ~= 0.10.1",
+        "instrumentation": "opentelemetry-instrumentation-pymysql==0.23.dev0",
+    },
+    "pyramid": {
+        "library": "pyramid >= 1.7",
+        "instrumentation": "opentelemetry-instrumentation-pyramid==0.23.dev0",
+    },
+    "redis": {
+        "library": "redis >= 2.6",
+        "instrumentation": "opentelemetry-instrumentation-redis==0.23.dev0",
+    },
+    "requests": {
+        "library": "requests ~= 2.0",
+        "instrumentation": "opentelemetry-instrumentation-requests==0.23.dev0",
+    },
+    "scikit-learn": {
+        "library": "scikit-learn ~= 0.24.0",
+        "instrumentation": "opentelemetry-instrumentation-sklearn==0.23.dev0",
+    },
+    "sqlalchemy": {
+        "library": "sqlalchemy >= 1.3",
+        "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.23.dev0",
+    },
+    "starlette": {
+        "library": "starlette ~= 0.13.0",
+        "instrumentation": "opentelemetry-instrumentation-starlette==0.23.dev0",
+    },
+    "tornado": {
+        "library": "tornado >= 6.0",
+        "instrumentation": "opentelemetry-instrumentation-tornado==0.23.dev0",
+    },
+    "urllib3": {
+        "library": "urllib3 >= 1.0.0, < 2.0.0",
+        "instrumentation": "opentelemetry-instrumentation-urllib3==0.23.dev0",
+    },
+}
+default_instrumentations = [
+    "opentelemetry-instrumentation-dbapi==0.23.dev0",
+    "opentelemetry-instrumentation-logging==0.23.dev0",
+    "opentelemetry-instrumentation-sqlite3==0.23.dev0",
+    "opentelemetry-instrumentation-urllib==0.23.dev0",
+    "opentelemetry-instrumentation-wsgi==0.23.dev0",
+]

From 0eabf8e7ec014266abd17c7e5d4002385c5a33c2 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 14:09:12 +0100
Subject: [PATCH 09/19] use pytest mark to skip tests. fix work directory for
 sqlalchemy tests

---
 .../tests/test_sqlalchemy.py                  | 37 +++++++++----------
 tox.ini                                       |  2 +-
 2 files changed, 18 insertions(+), 21 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
index 361e48fd09..84e38c5a54 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
@@ -11,10 +11,9 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import asyncio
-from typing import Coroutine
-from unittest import mock
+from unittest import IsolatedAsyncioTestCase, mock
 
+import pytest
 import sqlalchemy
 from sqlalchemy import create_engine
 
@@ -23,11 +22,7 @@
 from opentelemetry.test.test_base import TestBase
 
 
-def _call_async(coro: Coroutine):
-    return asyncio.get_event_loop().run_until_complete(coro)
-
-
-class TestSqlalchemyInstrumentation(TestBase):
+class TestSqlalchemyInstrumentation(TestBase, IsolatedAsyncioTestCase):
     def tearDown(self):
         super().tearDown()
         SQLAlchemyInstrumentor().uninstrument()
@@ -45,9 +40,11 @@ def test_trace_integration(self):
         self.assertEqual(spans[0].name, "SELECT :memory:")
         self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
 
-    def test_async_trace_integration(self):
-        if sqlalchemy.__version__.startswith("1.3"):
-            return
+    @pytest.mark.skipif(
+        sqlalchemy.__version__.startswith("1.3"),
+        reason="skipping async tests for 1.3",
+    )
+    async def test_async_trace_integration(self):
         from sqlalchemy.ext.asyncio import (  # pylint: disable-all
             create_async_engine,
         )
@@ -56,9 +53,8 @@ def test_async_trace_integration(self):
         SQLAlchemyInstrumentor().instrument(
             engine=engine.sync_engine, tracer_provider=self.tracer_provider
         )
-        cnx = _call_async(engine.connect())
-        _call_async(cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))).fetchall()
-        _call_async(cnx.close())
+        async with engine.connect() as cnx:
+            await cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))
         spans = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans), 1)
         self.assertEqual(spans[0].name, "SELECT :memory:")
@@ -95,18 +91,19 @@ def test_create_engine_wrapper(self):
         self.assertEqual(spans[0].name, "SELECT :memory:")
         self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
 
-    def test_create_async_engine_wrapper(self):
+    @pytest.mark.skipif(
+        sqlalchemy.__version__.startswith("1.3"),
+        reason="skipping async tests for 1.3",
+    )
+    async def test_create_async_engine_wrapper(self):
         SQLAlchemyInstrumentor().instrument()
-        if sqlalchemy.__version__.startswith("1.3"):
-            return
         from sqlalchemy.ext.asyncio import (  # pylint: disable-all
             create_async_engine,
         )
 
         engine = create_async_engine("sqlite+aiosqlite:///:memory:")
-        cnx = _call_async(engine.connect())
-        _call_async(cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))).fetchall()
-        _call_async(cnx.close())
+        async with engine.connect() as cnx:
+            await cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))
         spans = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans), 1)
         self.assertEqual(spans[0].name, "SELECT :memory:")
diff --git a/tox.ini b/tox.ini
index 568346aff3..5a9637f990 100644
--- a/tox.ini
+++ b/tox.ini
@@ -208,7 +208,7 @@ changedir =
   test-instrumentation-redis: instrumentation/opentelemetry-instrumentation-redis/tests
   test-instrumentation-requests: instrumentation/opentelemetry-instrumentation-requests/tests
   test-instrumentation-sklearn: instrumentation/opentelemetry-instrumentation-sklearn/tests
-  test-instrumentation-sqlalchemy: instrumentation/opentelemetry-instrumentation-sqlalchemy/tests
+  test-instrumentation-sqlalchemy{13,14}: instrumentation/opentelemetry-instrumentation-sqlalchemy/tests
   test-instrumentation-sqlite3: instrumentation/opentelemetry-instrumentation-sqlite3/tests
   test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests
   test-instrumentation-tornado: instrumentation/opentelemetry-instrumentation-tornado/tests

From 83937c0315303666cbb0526cefc4a58de1ae165f Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 14:11:13 +0100
Subject: [PATCH 10/19] fix lint issues

---
 .../tests/sqlalchemy_tests/test_mssql.py                       | 3 +--
 .../tests/sqlalchemy_tests/test_mysql.py                       | 3 +--
 .../tests/sqlalchemy_tests/test_postgres.py                    | 3 +--
 .../tests/sqlalchemy_tests/test_sqlite.py                      | 3 +--
 4 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py
index c48085a0e3..ef9cac051a 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mssql.py
@@ -85,8 +85,7 @@ def test_engine_execute_errors(self):
         self.assertTrue(span.end_time - span.start_time > 0)
         # check the error
         self.assertIs(
-            span.status.status_code,
-            trace.StatusCode.ERROR,
+            span.status.status_code, trace.StatusCode.ERROR,
         )
         self.assertIn("a_wrong_table", span.status.description)
 
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py
index 730939fd29..c9e0a8dd1e 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py
@@ -84,7 +84,6 @@ def test_engine_execute_errors(self):
         self.assertTrue(span.end_time - span.start_time > 0)
         # check the error
         self.assertIs(
-            span.status.status_code,
-            trace.StatusCode.ERROR,
+            span.status.status_code, trace.StatusCode.ERROR,
         )
         self.assertIn("a_wrong_table", span.status.description)
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py
index 62c880d140..d72791d970 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py
@@ -78,8 +78,7 @@ def test_engine_execute_errors(self):
         self.assertTrue(span.end_time - span.start_time > 0)
         # check the error
         self.assertIs(
-            span.status.status_code,
-            trace.StatusCode.ERROR,
+            span.status.status_code, trace.StatusCode.ERROR,
         )
         self.assertIn("a_wrong_table", span.status.description)
 
diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
index aa33b1ad0d..0acba0fec2 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py
@@ -54,8 +54,7 @@ def test_engine_execute_errors(self):
         self.assertTrue((span.end_time - span.start_time) > 0)
         # check the error
         self.assertIs(
-            span.status.status_code,
-            trace.StatusCode.ERROR,
+            span.status.status_code, trace.StatusCode.ERROR,
         )
         self.assertEqual(
             span.status.description, "no such table: a_wrong_table"

From 9164d9dfa2916eddcd522ee083b2b7ea5ac34164 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 14:36:12 +0100
Subject: [PATCH 11/19] address pylint

---
 .../instrumentation/sqlalchemy/engine.py      | 46 +++++++++----------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index f04ce33449..94c49a5f84 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -69,8 +69,8 @@ def __init__(self, tracer, engine):
         self.vendor = _normalize_vendor(engine.name)
 
         listen(engine, "before_cursor_execute", self._before_cur_exec)
-        listen(engine, "after_cursor_execute", self._after_cur_exec)
-        listen(engine, "handle_error", self._handle_error)
+        listen(engine, "after_cursor_execute", _after_cur_exec)
+        listen(engine, "handle_error", _handle_error)
 
     def _operation_name(self, db_name, statement):
         parts = []
@@ -109,29 +109,29 @@ def _before_cur_exec(
 
         context._span = span
 
-    # pylint: disable=unused-argument
-    def _after_cur_exec(
-        self, conn, cursor, statement, params, context, executemany
-    ):
-        span = getattr(context, "_span", None)
-        if span is None:
-            return
-
+# pylint: disable=unused-argument
+def _after_cur_exec(
+    conn, cursor, statement, params, context, executemany
+):
+    span = getattr(context, "_span", None)
+    if span is None:
+        return
+
+    span.end()
+
+def _handle_error(context):
+    span = getattr(context.execution_context, "_span", None)
+    if span is None:
+        return
+
+    try:
+        if span.is_recording():
+            span.set_status(
+                Status(StatusCode.ERROR, str(context.original_exception),)
+            )
+    finally:
         span.end()
 
-    def _handle_error(self, context):
-        span = getattr(context.execution_context, "_span", None)
-        if span is None:
-            return
-
-        try:
-            if span.is_recording():
-                span.set_status(
-                    Status(StatusCode.ERROR, str(context.original_exception),)
-                )
-        finally:
-            span.end()
-
 
 def _get_attributes_from_url(url):
     """Set connection tags from the url. return true if successful."""

From b51e4f1ccc9de446271f227492360bd3711aaa71 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 14:49:08 +0100
Subject: [PATCH 12/19] fix linter issues + generate

---
 .../src/opentelemetry/instrumentation/sqlalchemy/engine.py  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index 94c49a5f84..dd132b516f 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -109,16 +109,16 @@ def _before_cur_exec(
 
         context._span = span
 
+
 # pylint: disable=unused-argument
-def _after_cur_exec(
-    conn, cursor, statement, params, context, executemany
-):
+def _after_cur_exec(conn, cursor, statement, params, context, executemany):
     span = getattr(context, "_span", None)
     if span is None:
         return
 
     span.end()
 
+
 def _handle_error(context):
     span = getattr(context.execution_context, "_span", None)
     if span is None:

From 07b2f2b075cf3a600776f7ec7c99299adb5e4fac Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 15:10:45 +0100
Subject: [PATCH 13/19] remove IsolatedAsyncioTestCase as not supported in
 python < 3.8

---
 .../tests/test_sqlalchemy.py                  | 63 ++++++++++---------
 1 file changed, 35 insertions(+), 28 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
index 84e38c5a54..7bacda34cf 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
@@ -11,7 +11,8 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-from unittest import IsolatedAsyncioTestCase, mock
+import asyncio
+from unittest import mock
 
 import pytest
 import sqlalchemy
@@ -22,7 +23,7 @@
 from opentelemetry.test.test_base import TestBase
 
 
-class TestSqlalchemyInstrumentation(TestBase, IsolatedAsyncioTestCase):
+class TestSqlalchemyInstrumentation(TestBase):
     def tearDown(self):
         super().tearDown()
         SQLAlchemyInstrumentor().uninstrument()
@@ -44,21 +45,24 @@ def test_trace_integration(self):
         sqlalchemy.__version__.startswith("1.3"),
         reason="skipping async tests for 1.3",
     )
-    async def test_async_trace_integration(self):
-        from sqlalchemy.ext.asyncio import (  # pylint: disable-all
-            create_async_engine,
-        )
+    def test_async_trace_integration(self):
+        async def run():
+            from sqlalchemy.ext.asyncio import (  # pylint: disable-all
+                create_async_engine,
+            )
 
-        engine = create_async_engine("sqlite+aiosqlite:///:memory:")
-        SQLAlchemyInstrumentor().instrument(
-            engine=engine.sync_engine, tracer_provider=self.tracer_provider
-        )
-        async with engine.connect() as cnx:
-            await cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))
-        spans = self.memory_exporter.get_finished_spans()
-        self.assertEqual(len(spans), 1)
-        self.assertEqual(spans[0].name, "SELECT :memory:")
-        self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
+            engine = create_async_engine("sqlite+aiosqlite:///:memory:")
+            SQLAlchemyInstrumentor().instrument(
+                engine=engine.sync_engine, tracer_provider=self.tracer_provider
+            )
+            async with engine.connect() as cnx:
+                await cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))
+            spans = self.memory_exporter.get_finished_spans()
+            self.assertEqual(len(spans), 1)
+            self.assertEqual(spans[0].name, "SELECT :memory:")
+            self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
+
+        asyncio.get_event_loop().run_until_complete(run())
 
     def test_not_recording(self):
         mock_tracer = mock.Mock()
@@ -95,16 +99,19 @@ def test_create_engine_wrapper(self):
         sqlalchemy.__version__.startswith("1.3"),
         reason="skipping async tests for 1.3",
     )
-    async def test_create_async_engine_wrapper(self):
-        SQLAlchemyInstrumentor().instrument()
-        from sqlalchemy.ext.asyncio import (  # pylint: disable-all
-            create_async_engine,
-        )
+    def test_create_async_engine_wrapper(self):
+        async def run():
+            SQLAlchemyInstrumentor().instrument()
+            from sqlalchemy.ext.asyncio import (  # pylint: disable-all
+                create_async_engine,
+            )
 
-        engine = create_async_engine("sqlite+aiosqlite:///:memory:")
-        async with engine.connect() as cnx:
-            await cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))
-        spans = self.memory_exporter.get_finished_spans()
-        self.assertEqual(len(spans), 1)
-        self.assertEqual(spans[0].name, "SELECT :memory:")
-        self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
+            engine = create_async_engine("sqlite+aiosqlite:///:memory:")
+            async with engine.connect() as cnx:
+                await cnx.execute(sqlalchemy.text("SELECT	1 + 1;"))
+            spans = self.memory_exporter.get_finished_spans()
+            self.assertEqual(len(spans), 1)
+            self.assertEqual(spans[0].name, "SELECT :memory:")
+            self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
+
+        asyncio.get_event_loop().run_until_complete(run())

From f1985f6a19c7d975f365c93243ec5e656c9f34ec Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 7 Jul 2021 17:29:51 +0100
Subject: [PATCH 14/19] update CHANGELOG

---
 CHANGELOG.md | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fccf35d963..f522cd8ff2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,6 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   ([#563](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/563))
 - `opentelemetry-exporter-datadog` Datadog exporter should not use `unknown_service` as fallback resource service name.
   ([#570](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/570))
+- Add support for the async extension of SQLAlchemy (>= 1.4)
+  ([#568](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/568))
 
 ### Added
 - `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation

From ae8ce9b46a65e21af016665c7ecb28ed95c6bbb3 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Thu, 8 Jul 2021 11:08:18 +0100
Subject: [PATCH 15/19] address PR comments

---
 .../instrumentation/sqlalchemy/engine.py       | 18 ++++++++----------
 .../instrumentation/sqlalchemy/package.py      |  2 +-
 .../tests/test_sqlalchemy.py                   |  8 ++++----
 .../instrumentation/bootstrap_gen.py           |  2 +-
 4 files changed, 14 insertions(+), 16 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
index dd132b516f..ed1dfb1976 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py
@@ -107,12 +107,12 @@ def _before_cur_exec(
                 for key, value in attrs.items():
                     span.set_attribute(key, value)
 
-        context._span = span
+        context._otel_span = span
 
 
 # pylint: disable=unused-argument
 def _after_cur_exec(conn, cursor, statement, params, context, executemany):
-    span = getattr(context, "_span", None)
+    span = getattr(context, "_otel_span", None)
     if span is None:
         return
 
@@ -120,17 +120,15 @@ def _after_cur_exec(conn, cursor, statement, params, context, executemany):
 
 
 def _handle_error(context):
-    span = getattr(context.execution_context, "_span", None)
+    span = getattr(context.execution_context, "_otel_span", None)
     if span is None:
         return
 
-    try:
-        if span.is_recording():
-            span.set_status(
-                Status(StatusCode.ERROR, str(context.original_exception),)
-            )
-    finally:
-        span.end()
+    if span.is_recording():
+        span.set_status(
+            Status(StatusCode.ERROR, str(context.original_exception),)
+        )
+    span.end()
 
 
 def _get_attributes_from_url(url):
diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py
index c79b14cfd7..d608d3476a 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/package.py
@@ -13,4 +13,4 @@
 # limitations under the License.
 
 
-_instruments = ("sqlalchemy >= 1.3",)
+_instruments = ("sqlalchemy",)
diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
index 7bacda34cf..fd1d11e7d0 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
@@ -42,8 +42,8 @@ def test_trace_integration(self):
         self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
 
     @pytest.mark.skipif(
-        sqlalchemy.__version__.startswith("1.3"),
-        reason="skipping async tests for 1.3",
+        not sqlalchemy.__version__.startswith("1.4"),
+        reason="on run async tests for 1.4",
     )
     def test_async_trace_integration(self):
         async def run():
@@ -96,8 +96,8 @@ def test_create_engine_wrapper(self):
         self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
 
     @pytest.mark.skipif(
-        sqlalchemy.__version__.startswith("1.3"),
-        reason="skipping async tests for 1.3",
+        not sqlalchemy.__version__.startswith("1.4"),
+        reason="on run async tests for 1.4",
     )
     def test_create_async_engine_wrapper(self):
         async def run():
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
index ccbf116e35..b49f40905f 100644
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
@@ -113,7 +113,7 @@
         "instrumentation": "opentelemetry-instrumentation-sklearn==0.23.dev0",
     },
     "sqlalchemy": {
-        "library": "sqlalchemy >= 1.3",
+        "library": "sqlalchemy",
         "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.23.dev0",
     },
     "starlette": {

From 3a7c84da2687ca9b2e8d484aba6f1fb28fba2a2b Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Sat, 10 Jul 2021 22:49:39 +0100
Subject: [PATCH 16/19] add comment about execute_values for psycopg2 dialect

---
 .../tests/sqlalchemy_tests/mixins.py                          | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
index 1493ce44cb..b9c766ad1c 100644
--- a/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
+++ b/tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py
@@ -243,6 +243,10 @@ def insert_players(session):
                 close_all_sessions()
 
         spans = self.memory_exporter.get_finished_spans()
+
+        # SQLAlchemy 1.4 uses the `execute_values` extension of the psycopg2 dialect to
+        # batch inserts together which means `insert_players` only generates one span.
+        # See https://docs.sqlalchemy.org/en/14/changelog/migration_14.html#orm-batch-inserts-with-psycopg2-now-batch-statements-with-returning-in-most-cases
         self.assertEqual(
             len(spans), 5 if self.VENDOR not in ["postgresql"] else 3
         )

From 5e38f5a145296180cda28585f5e8b7f34e88f016 Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Wed, 14 Jul 2021 11:38:23 +0100
Subject: [PATCH 17/19] use SQLAlchemy 1.1 for tests

---
 .../tests/test_sqlalchemy.py                           |  4 ++--
 tox.ini                                                | 10 +++++-----
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
index fd1d11e7d0..bed2b5f312 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py
@@ -43,7 +43,7 @@ def test_trace_integration(self):
 
     @pytest.mark.skipif(
         not sqlalchemy.__version__.startswith("1.4"),
-        reason="on run async tests for 1.4",
+        reason="only run async tests for 1.4",
     )
     def test_async_trace_integration(self):
         async def run():
@@ -97,7 +97,7 @@ def test_create_engine_wrapper(self):
 
     @pytest.mark.skipif(
         not sqlalchemy.__version__.startswith("1.4"),
-        reason="on run async tests for 1.4",
+        reason="only run async tests for 1.4",
     )
     def test_create_async_engine_wrapper(self):
         async def run():
diff --git a/tox.ini b/tox.ini
index 5a9637f990..e7a5445043 100644
--- a/tox.ini
+++ b/tox.ini
@@ -122,8 +122,8 @@ envlist =
     py3{6,7,8,9}-test-instrumentation-grpc
 
     ; opentelemetry-instrumentation-sqlalchemy
-    py3{6,7,8,9}-test-instrumentation-sqlalchemy{13,14}
-    pypy3-test-instrumentation-sqlalchemy{13,14}
+    py3{6,7,8,9}-test-instrumentation-sqlalchemy{11,14}
+    pypy3-test-instrumentation-sqlalchemy{11,14}
 
     ; opentelemetry-instrumentation-redis
     py3{6,7,8,9}-test-instrumentation-redis
@@ -173,7 +173,7 @@ deps =
   elasticsearch6: elasticsearch>=6.0,<7.0
   elasticsearch7: elasticsearch-dsl>=7.0,<8.0
   elasticsearch7: elasticsearch>=7.0,<8.0
-  sqlalchemy13: sqlalchemy~=1.3,<1.4
+  sqlalchemy11: sqlalchemy>=1.1,<1.2
   sqlalchemy14: aiosqlite
   sqlalchemy14: sqlalchemy~=1.4
 
@@ -208,7 +208,7 @@ changedir =
   test-instrumentation-redis: instrumentation/opentelemetry-instrumentation-redis/tests
   test-instrumentation-requests: instrumentation/opentelemetry-instrumentation-requests/tests
   test-instrumentation-sklearn: instrumentation/opentelemetry-instrumentation-sklearn/tests
-  test-instrumentation-sqlalchemy{13,14}: instrumentation/opentelemetry-instrumentation-sqlalchemy/tests
+  test-instrumentation-sqlalchemy{11,14}: instrumentation/opentelemetry-instrumentation-sqlalchemy/tests
   test-instrumentation-sqlite3: instrumentation/opentelemetry-instrumentation-sqlite3/tests
   test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests
   test-instrumentation-tornado: instrumentation/opentelemetry-instrumentation-tornado/tests
@@ -293,7 +293,7 @@ commands_pre =
 
   sklearn: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test]
 
-  sqlalchemy{13,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test]
+  sqlalchemy{11,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test]
 
   elasticsearch{2,5,6,7}: pip install {toxinidir}/opentelemetry-python-core/opentelemetry-instrumentation {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test]
 

From 83e0e4f71df40f73b3aa5313d5cac994d2912dac Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Sat, 24 Jul 2021 11:29:44 +0100
Subject: [PATCH 18/19] remove gen file

---
 .../instrumentation/bootstrap_gen.py          | 138 ------------------
 1 file changed, 138 deletions(-)
 delete mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py

diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
deleted file mode 100644
index b49f40905f..0000000000
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# Copyright The OpenTelemetry Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES.
-# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE.
-
-libraries = {
-    "aiohttp": {
-        "library": "aiohttp ~= 3.0",
-        "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.23.dev0",
-    },
-    "aiopg": {
-        "library": "aiopg >= 0.13.0, < 1.3.0",
-        "instrumentation": "opentelemetry-instrumentation-aiopg==0.23.dev0",
-    },
-    "asgiref": {
-        "library": "asgiref ~= 3.0",
-        "instrumentation": "opentelemetry-instrumentation-asgi==0.23.dev0",
-    },
-    "asyncpg": {
-        "library": "asyncpg >= 0.12.0",
-        "instrumentation": "opentelemetry-instrumentation-asyncpg==0.23.dev0",
-    },
-    "boto": {
-        "library": "boto~=2.0",
-        "instrumentation": "opentelemetry-instrumentation-boto==0.23.dev0",
-    },
-    "botocore": {
-        "library": "botocore ~= 1.0",
-        "instrumentation": "opentelemetry-instrumentation-botocore==0.23.dev0",
-    },
-    "celery": {
-        "library": "celery >= 4.0, < 6.0",
-        "instrumentation": "opentelemetry-instrumentation-celery==0.23.dev0",
-    },
-    "django": {
-        "library": "django >= 1.10",
-        "instrumentation": "opentelemetry-instrumentation-django==0.23.dev0",
-    },
-    "elasticsearch": {
-        "library": "elasticsearch >= 2.0",
-        "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.23.dev0",
-    },
-    "falcon": {
-        "library": "falcon ~= 2.0",
-        "instrumentation": "opentelemetry-instrumentation-falcon==0.23.dev0",
-    },
-    "fastapi": {
-        "library": "fastapi ~= 0.58.1",
-        "instrumentation": "opentelemetry-instrumentation-fastapi==0.23.dev0",
-    },
-    "flask": {
-        "library": "flask >= 1.0, < 3.0",
-        "instrumentation": "opentelemetry-instrumentation-flask==0.23.dev0",
-    },
-    "grpcio": {
-        "library": "grpcio ~= 1.27",
-        "instrumentation": "opentelemetry-instrumentation-grpc==0.23.dev0",
-    },
-    "httpx": {
-        "library": "httpx >= 0.18.0, < 0.19.0",
-        "instrumentation": "opentelemetry-instrumentation-httpx==0.23.dev0",
-    },
-    "jinja2": {
-        "library": "jinja2~=2.7",
-        "instrumentation": "opentelemetry-instrumentation-jinja2==0.23.dev0",
-    },
-    "mysql-connector-python": {
-        "library": "mysql-connector-python ~= 8.0",
-        "instrumentation": "opentelemetry-instrumentation-mysql==0.23.dev0",
-    },
-    "psycopg2": {
-        "library": "psycopg2 >= 2.7.3.1",
-        "instrumentation": "opentelemetry-instrumentation-psycopg2==0.23.dev0",
-    },
-    "pymemcache": {
-        "library": "pymemcache ~= 1.3",
-        "instrumentation": "opentelemetry-instrumentation-pymemcache==0.23.dev0",
-    },
-    "pymongo": {
-        "library": "pymongo ~= 3.1",
-        "instrumentation": "opentelemetry-instrumentation-pymongo==0.23.dev0",
-    },
-    "PyMySQL": {
-        "library": "PyMySQL ~= 0.10.1",
-        "instrumentation": "opentelemetry-instrumentation-pymysql==0.23.dev0",
-    },
-    "pyramid": {
-        "library": "pyramid >= 1.7",
-        "instrumentation": "opentelemetry-instrumentation-pyramid==0.23.dev0",
-    },
-    "redis": {
-        "library": "redis >= 2.6",
-        "instrumentation": "opentelemetry-instrumentation-redis==0.23.dev0",
-    },
-    "requests": {
-        "library": "requests ~= 2.0",
-        "instrumentation": "opentelemetry-instrumentation-requests==0.23.dev0",
-    },
-    "scikit-learn": {
-        "library": "scikit-learn ~= 0.24.0",
-        "instrumentation": "opentelemetry-instrumentation-sklearn==0.23.dev0",
-    },
-    "sqlalchemy": {
-        "library": "sqlalchemy",
-        "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.23.dev0",
-    },
-    "starlette": {
-        "library": "starlette ~= 0.13.0",
-        "instrumentation": "opentelemetry-instrumentation-starlette==0.23.dev0",
-    },
-    "tornado": {
-        "library": "tornado >= 6.0",
-        "instrumentation": "opentelemetry-instrumentation-tornado==0.23.dev0",
-    },
-    "urllib3": {
-        "library": "urllib3 >= 1.0.0, < 2.0.0",
-        "instrumentation": "opentelemetry-instrumentation-urllib3==0.23.dev0",
-    },
-}
-default_instrumentations = [
-    "opentelemetry-instrumentation-dbapi==0.23.dev0",
-    "opentelemetry-instrumentation-logging==0.23.dev0",
-    "opentelemetry-instrumentation-sqlite3==0.23.dev0",
-    "opentelemetry-instrumentation-urllib==0.23.dev0",
-    "opentelemetry-instrumentation-wsgi==0.23.dev0",
-]

From 125de32a2c9e5a6d59e47b48f043bba8d0ffcc5c Mon Sep 17 00:00:00 2001
From: Matthew Brown <me@matthewbrown.io>
Date: Sat, 24 Jul 2021 11:43:05 +0100
Subject: [PATCH 19/19] use packaging to test versions

---
 .../src/opentelemetry/instrumentation/sqlalchemy/__init__.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
index b5816a38d4..05e6451626 100644
--- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py
@@ -54,6 +54,7 @@
 from typing import Collection
 
 import sqlalchemy
+from packaging.version import parse as parse_version
 from wrapt import wrap_function_wrapper as _w
 
 from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
@@ -89,7 +90,7 @@ def _instrument(self, **kwargs):
         """
         _w("sqlalchemy", "create_engine", _wrap_create_engine)
         _w("sqlalchemy.engine", "create_engine", _wrap_create_engine)
-        if sqlalchemy.__version__.startswith("1.4"):
+        if parse_version(sqlalchemy.__version__).release >= (1, 4):
             _w(
                 "sqlalchemy.ext.asyncio",
                 "create_async_engine",
@@ -108,5 +109,5 @@ def _instrument(self, **kwargs):
     def _uninstrument(self, **kwargs):
         unwrap(sqlalchemy, "create_engine")
         unwrap(sqlalchemy.engine, "create_engine")
-        if sqlalchemy.__version__.startswith("1.4"):
+        if parse_version(sqlalchemy.__version__).release >= (1, 4):
             unwrap(sqlalchemy.ext.asyncio, "create_async_engine")