Repository: open-telemetry/opentelemetry-python

Issue:
Add instrumentation for sqlite3
Add an instrumentation for sqlite3.

[This](https://github.com/open-telemetry/opentelemetry-python-contrib/tree/master/reference/ddtrace/contrib/sqlite3) implementation is a good reference.


Discussion:
@ocelotl could you assign this to me?
@cnnradams done

Resolving patch:
diff --git a/ext/opentelemetry-ext-sqlite3/setup.py b/ext/opentelemetry-ext-sqlite3/setup.py
new file mode 100644
--- /dev/null
+++ b/ext/opentelemetry-ext-sqlite3/setup.py
@@ -0,0 +1,26 @@
+# 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.
+import os
+
+import setuptools
+
+BASE_DIR = os.path.dirname(__file__)
+VERSION_FILENAME = os.path.join(
+    BASE_DIR, "src", "opentelemetry", "ext", "sqlite3", "version.py"
+)
+PACKAGE_INFO = {}
+with open(VERSION_FILENAME) as f:
+    exec(f.read(), PACKAGE_INFO)
+
+setuptools.setup(version=PACKAGE_INFO["__version__"])
diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py
new file mode 100644
--- /dev/null
+++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/__init__.py
@@ -0,0 +1,111 @@
+# 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.
+
+"""
+SQLite instrumentation supporting `sqlite3`_, it can be enabled by
+using ``SQLite3Instrumentor``.
+
+.. _sqlite3: https://docs.python.org/3/library/sqlite3.html
+
+Usage
+-----
+
+.. code:: python
+
+    import sqlite3
+    from opentelemetry import trace
+    from opentelemetry.trace import TracerProvider
+    from opentelemetry.ext.sqlite3 import SQLite3Instrumentor
+
+    trace.set_tracer_provider(TracerProvider())
+
+    SQLite3Instrumentor().instrument()
+
+    cnx = sqlite3.connect('example.db')
+    cursor = cnx.cursor()
+    cursor.execute("INSERT INTO test (testField) VALUES (123)")
+    cursor.close()
+    cnx.close()
+
+API
+---
+"""
+
+import sqlite3
+import typing
+
+from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.ext import dbapi
+from opentelemetry.ext.sqlite3.version import __version__
+from opentelemetry.trace import TracerProvider, get_tracer
+
+
+class SQLite3Instrumentor(BaseInstrumentor):
+    # No useful attributes of sqlite3 connection object
+    _CONNECTION_ATTRIBUTES = {}
+
+    _DATABASE_COMPONENT = "sqlite3"
+    _DATABASE_TYPE = "sql"
+
+    def _instrument(self, **kwargs):
+        """Integrate with SQLite3 Python library.
+        https://docs.python.org/3/library/sqlite3.html
+        """
+        tracer_provider = kwargs.get("tracer_provider")
+
+        tracer = get_tracer(__name__, __version__, tracer_provider)
+
+        dbapi.wrap_connect(
+            tracer,
+            sqlite3,
+            "connect",
+            self._DATABASE_COMPONENT,
+            self._DATABASE_TYPE,
+            self._CONNECTION_ATTRIBUTES,
+        )
+
+    def _uninstrument(self, **kwargs):
+        """"Disable SQLite3 instrumentation"""
+        dbapi.unwrap_connect(sqlite3, "connect")
+
+    # pylint:disable=no-self-use
+    def instrument_connection(self, connection):
+        """Enable instrumentation in a SQLite connection.
+
+        Args:
+            connection: The connection to instrument.
+
+        Returns:
+            An instrumented connection.
+        """
+        tracer = get_tracer(__name__, __version__)
+
+        return dbapi.instrument_connection(
+            tracer,
+            connection,
+            self._DATABASE_COMPONENT,
+            self._DATABASE_TYPE,
+            self._CONNECTION_ATTRIBUTES,
+        )
+
+    def uninstrument_connection(self, connection):
+        """Disable instrumentation in a SQLite connection.
+
+        Args:
+            connection: The connection to uninstrument.
+
+        Returns:
+            An uninstrumented connection.
+        """
+        return dbapi.uninstrument_connection(connection)
diff --git a/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py
new file mode 100644
--- /dev/null
+++ b/ext/opentelemetry-ext-sqlite3/src/opentelemetry/ext/sqlite3/version.py
@@ -0,0 +1,15 @@
+# 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.
+
+__version__ = "0.8.dev0"


Test patch:
diff --git a/ext/opentelemetry-ext-sqlite3/tests/__init__.py b/ext/opentelemetry-ext-sqlite3/tests/__init__.py
new file mode 100644
diff --git a/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py b/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py
new file mode 100644
--- /dev/null
+++ b/ext/opentelemetry-ext-sqlite3/tests/test_sqlite3.py
@@ -0,0 +1,83 @@
+# Copyright 2020, 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.
+
+import sqlite3
+
+from opentelemetry import trace as trace_api
+from opentelemetry.ext.sqlite3 import SQLite3Instrumentor
+from opentelemetry.test.test_base import TestBase
+
+
+class TestSQLite3(TestBase):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls._connection = None
+        cls._cursor = None
+        cls._tracer = cls.tracer_provider.get_tracer(__name__)
+        SQLite3Instrumentor().instrument(tracer_provider=cls.tracer_provider)
+        cls._connection = sqlite3.connect(":memory:")
+        cls._cursor = cls._connection.cursor()
+
+    @classmethod
+    def tearDownClass(cls):
+        if cls._cursor:
+            cls._cursor.close()
+        if cls._connection:
+            cls._connection.close()
+
+    def validate_spans(self):
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 2)
+        for span in spans:
+            if span.name == "rootSpan":
+                root_span = span
+            else:
+                child_span = span
+            self.assertIsInstance(span.start_time, int)
+            self.assertIsInstance(span.end_time, int)
+        self.assertIsNotNone(root_span)
+        self.assertIsNotNone(child_span)
+        self.assertEqual(root_span.name, "rootSpan")
+        self.assertEqual(child_span.name, "sqlite3")
+        self.assertIsNotNone(child_span.parent)
+        self.assertIs(child_span.parent, root_span.get_context())
+        self.assertIs(child_span.kind, trace_api.SpanKind.CLIENT)
+
+    def test_execute(self):
+        """Should create a child span for execute method
+        """
+        with self._tracer.start_as_current_span("rootSpan"):
+            self._cursor.execute(
+                "CREATE TABLE IF NOT EXISTS test (id integer)"
+            )
+        self.validate_spans()
+
+    def test_executemany(self):
+        """Should create a child span for executemany
+        """
+        with self._tracer.start_as_current_span("rootSpan"):
+            data = [("1",), ("2",), ("3",)]
+            stmt = "INSERT INTO test (id) VALUES (?)"
+            self._cursor.executemany(stmt, data)
+        self.validate_spans()
+
+    def test_callproc(self):
+        """Should create a child span for callproc
+        """
+        with self._tracer.start_as_current_span("rootSpan"), self.assertRaises(
+            Exception
+        ):
+            self._cursor.callproc("test", ())
+            self.validate_spans()