From 4d68edb8f97efc027bb8a7e3ffc6bd5e99338b38 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Sun, 19 Sep 2021 11:33:53 +1000 Subject: [PATCH] Tortoise ORM instrumentation --- .../README.rst | 22 ++ .../setup.cfg | 54 +++++ .../setup.py | 99 ++++++++ .../instrumentation/tortoiseorm/__init__.py | 214 ++++++++++++++++++ .../instrumentation/tortoiseorm/package.py | 16 ++ .../instrumentation/tortoiseorm/version.py | 15 ++ 6 files changed, 420 insertions(+) create mode 100644 instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.cfg create mode 100644 instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.py create mode 100644 instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/package.py create mode 100644 instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/version.py diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst b/instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst new file mode 100644 index 0000000000..57fc0c4aab --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/README.rst @@ -0,0 +1,22 @@ +OpenTelemetry Tortoise ORM Instrumentation +========================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-tortoiseorm.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-tortoiseorm/ + +This library allows tracing queries made by tortoise ORM backends, mysql, postgres and sqlite. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-tortoiseorm + +References +---------- + +* `OpenTelemetry Project `_ +* `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.cfg b/instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.cfg new file mode 100644 index 0000000000..548f2d558d --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.cfg @@ -0,0 +1,54 @@ +# 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. +# +[metadata] +name = opentelemetry-instrumentation-tortoiseorm +description = OpenTelemetry instrumentation for Tortoise ORM +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-tortoiseorm +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +python_requires = >=3.6 +package_dir= + =src +packages=find_namespace: +install_requires = + opentelemetry-api ~= 1.3 + opentelemetry-semantic-conventions == 0.24b0 + opentelemetry-instrumentation == 0.24b0 + +[options.extras_require] +test = + opentelemetry-test == 0.24b0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + tortoise = opentelemetry.instrumentation.tortoiseorm:TortoiseORMInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.py new file mode 100644 index 0000000000..cfe0124740 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/setup.py @@ -0,0 +1,99 @@ +# 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 templates/instrumentation_setup.py.txt. +# RUN `python scripts/generate_setup.py` TO REGENERATE. + + +import distutils.cmd +import json +import os +from configparser import ConfigParser + +import setuptools + +config = ConfigParser() +config.read("setup.cfg") + +# We provide extras_require parameter to setuptools.setup later which +# overwrites the extra_require section from setup.cfg. To support extra_require +# secion in setup.cfg, we load it here and merge it with the extra_require param. +extras_require = {} +if "options.extras_require" in config: + for key, value in config["options.extras_require"].items(): + extras_require[key] = [v for v in value.split("\n") if v.strip()] + +BASE_DIR = os.path.dirname(__file__) +PACKAGE_INFO = {} + +VERSION_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "tortoiseorm", + "version.py", +) +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +PACKAGE_FILENAME = os.path.join( + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "tortoiseorm", + "package.py", +) +with open(PACKAGE_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +# Mark any instruments/runtime dependencies as test dependencies as well. +extras_require["instruments"] = PACKAGE_INFO["_instruments"] +test_deps = extras_require.get("test", []) +for dep in extras_require["instruments"]: + test_deps.append(dep) + +extras_require["test"] = test_deps + + +class JSONMetadataCommand(distutils.cmd.Command): + + description = ( + "print out package metadata as JSON. This is used by OpenTelemetry dev scripts to ", + "auto-generate code in other places", + ) + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + metadata = { + "name": config["metadata"]["name"], + "version": PACKAGE_INFO["__version__"], + "instruments": PACKAGE_INFO["_instruments"], + } + print(json.dumps(metadata)) + + +setuptools.setup( + cmdclass={"meta": JSONMetadataCommand}, + version=PACKAGE_INFO["__version__"], + extras_require=extras_require, +) diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py new file mode 100644 index 0000000000..6533e82919 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/__init__.py @@ -0,0 +1,214 @@ +# 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. + +""" +Instrument `tortoise-orm`_ to report SQL queries. +Usage +----- +.. code:: python + from opentelemetry.instrumentation.tortoiseorm import TortoiseORMInstrumentor + from tortoise.contrib.fastapi import register_tortoise + + register_tortoise( + app, + db_url=settings.db_url, + modules={"models": ["example_app.db_models"]}, + generate_schemas=True, + add_exception_handlers=True, + ) + + TortoiseORMInstrumentor().instrument(tracer_provider=tracer) +API +--- +""" +from typing import Collection + +try: + import tortoise.backends.asyncpg.client + + TORTOISE_POSTGRES_SUPPORT = True +except ModuleNotFoundError: + TORTOISE_POSTGRES_SUPPORT = False + +try: + import tortoise.backends.mysql.client + + TORTOISE_MYSQL_SUPPORT = True +except ModuleNotFoundError: + TORTOISE_MYSQL_SUPPORT = False + +try: + import tortoise.backends.sqlite.client + + TORTOISE_SQLITE_SUPPORT = True +except ModuleNotFoundError: + TORTOISE_SQLITE_SUPPORT = False + +import wrapt + +from opentelemetry import trace +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.tortoiseorm.package import _instruments +from opentelemetry.instrumentation.tortoiseorm.version import __version__ +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes +from opentelemetry.trace import SpanKind +from opentelemetry.trace.status import Status, StatusCode + + +def _hydrate_span_from_args(connection, query, parameters) -> dict: + """Get network and database attributes from connection.""" + span_attributes = {} + capabilities = getattr(connection, "capabilities", None) + if capabilities: + if capabilities.dialect == "sqlite": + span_attributes[SpanAttributes.DB_SYSTEM] = DbSystemValues.SQLITE.value + elif capabilities.dialect == "postgres": + span_attributes[SpanAttributes.DB_SYSTEM] = DbSystemValues.POSTGRESQL.value + elif capabilities.dialect == "mysql": + span_attributes[SpanAttributes.DB_SYSTEM] = DbSystemValues.MYSQL.value + dbname = getattr(connection, "filename", None) + if dbname: + span_attributes[SpanAttributes.DB_NAME] = dbname + dbname = getattr(connection, "database", None) + if dbname: + span_attributes[SpanAttributes.DB_NAME] = dbname + if query is not None: + span_attributes[SpanAttributes.DB_STATEMENT] = query + user = getattr(connection, "user", None) + if user: + span_attributes[SpanAttributes.DB_USER] = user + host = getattr(connection, "host", None) + if host: + span_attributes[SpanAttributes.NET_PEER_NAME] = host + port = getattr(connection, "port", None) + if port: + span_attributes[SpanAttributes.NET_PEER_PORT] = port + + if parameters is not None and len(parameters) > 0: + span_attributes["db.statement.parameters"] = str(parameters) + + return span_attributes + + +class TortoiseORMInstrumentor(BaseInstrumentor): + """An instrumentor for Tortoise-ORM + See `BaseInstrumentor` + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + """Instruments Tortoise ORM backend methods. + Args: + **kwargs: Optional arguments + ``tracer_provider``: a TracerProvider, defaults to global + Returns: + None + """ + tracer_provider = kwargs.get("tracer_provider") + self._tracer = trace.get_tracer(__name__, __version__, tracer_provider) + if TORTOISE_SQLITE_SUPPORT: + funcs = [ + "SqliteClient.execute_many", + "SqliteClient.execute_query", + "SqliteClient.execute_insert", + "SqliteClient.execute_query_dict", + "SqliteClient.execute_script", + ] + for f in funcs: + wrapt.wrap_function_wrapper( + "tortoise.backends.sqlite.client", + f, + self._do_execute, + ) + + if TORTOISE_POSTGRES_SUPPORT: + funcs = [ + "AsyncpgDBClient.execute_many", + "AsyncpgDBClient.execute_query", + "AsyncpgDBClient.execute_insert", + "AsyncpgDBClient.execute_query_dict", + "AsyncpgDBClient.execute_script", + ] + for f in funcs: + wrapt.wrap_function_wrapper( + "tortoise.backends.asyncpg.client", + f, + self._do_execute, + ) + + if TORTOISE_MYSQL_SUPPORT: + funcs = [ + "MySQLClient.execute_many", + "MySQLClient.execute_query", + "MySQLClient.execute_insert", + "MySQLClient.execute_query_dict", + "MySQLClient.execute_script", + ] + for f in funcs: + wrapt.wrap_function_wrapper( + "tortoise.backends.mysql.client", + f, + self._do_execute, + ) + + def _uninstrument(self, **kwargs): + if TORTOISE_SQLITE_SUPPORT: + unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_query") + unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_many") + unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_insert") + unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_query_dict") + unwrap(tortoise.backends.sqlite.client.SqliteClient, "execute_script") + if TORTOISE_MYSQL_SUPPORT: + unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_query") + unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_many") + unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_insert") + unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_query_dict") + unwrap(tortoise.backends.mysql.client.MySQLClient, "execute_script") + if self.TORTOISE_POSTGRES_SUPPORT: + unwrap(tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_query") + unwrap(tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_many") + unwrap(tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_insert") + unwrap( + tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_query_dict" + ) + unwrap(tortoise.backends.asyncpg.client.AsyncpgDBClient, "execute_script") + + async def _do_execute(self, func, instance, args, kwargs): + + exception = None + name = args[0] + + with self._tracer.start_as_current_span(name, kind=SpanKind.CLIENT) as span: + if span.is_recording(): + span_attributes = _hydrate_span_from_args( + instance, + args[0], + args[1:], + ) + for attribute, value in span_attributes.items(): + span.set_attribute(attribute, value) + + try: + result = await func(*args, **kwargs) + except Exception as exc: # pylint: disable=W0703 + exception = exc + raise + finally: + if span.is_recording() and exception is not None: + span.set_status(Status(StatusCode.ERROR)) + + return result diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/package.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/package.py new file mode 100644 index 0000000000..12e8c86144 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/package.py @@ -0,0 +1,16 @@ +# 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. + + +_instruments = ("tortoise-orm >= 0.17.0",) diff --git a/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/version.py b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/version.py new file mode 100644 index 0000000000..d33bd87ce4 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-tortoiseorm/src/opentelemetry/instrumentation/tortoiseorm/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.24b0"