Skip to content

Commit

Permalink
Adding DB API integration + MySQL connector integration (#264)
Browse files Browse the repository at this point in the history
Adding the ext.dbapi and ext.mysql package.
  • Loading branch information
hectorhdzg authored and toumorokoshi committed Jan 5, 2020
1 parent b72cab5 commit b01f7e8
Show file tree
Hide file tree
Showing 15 changed files with 731 additions and 4 deletions.
25 changes: 25 additions & 0 deletions ext/opentelemetry-ext-dbapi/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
OpenTelemetry Database API integration
=================================

The trace integration with Database API supports libraries following the specification.

.. PEP 249 -- Python Database API Specification v2.0: https://www.python.org/dev/peps/pep-0249/
Usage
-----

.. code:: python
import mysql.connector
from opentelemetry.trace import tracer
from opentelemetry.ext.dbapi import trace_integration
# Ex: mysql.connector
trace_integration(tracer(), mysql.connector, "connect", "mysql")
References
----------

* `OpenTelemetry Project <https://opentelemetry.io/>`_
46 changes: 46 additions & 0 deletions ext/opentelemetry-ext-dbapi/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2019, 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-ext-dbapi
description = OpenTelemetry Database API integration
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/ext/opentelemetry-ext-dbapi
platforms = any
license = Apache-2.0
classifiers =
Development Status :: 3 - Alpha
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7

[options]
python_requires = >=3.4
package_dir=
=src
packages=find_namespace:
install_requires =
opentelemetry-api >= 0.4.dev0
wrapt >= 1.0.0, < 2.0.0

[options.packages.find]
where = src
26 changes: 26 additions & 0 deletions ext/opentelemetry-ext-dbapi/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2019, 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", "dbapi", "version.py"
)
PACKAGE_INFO = {}
with open(VERSION_FILENAME) as f:
exec(f.read(), PACKAGE_INFO)

setuptools.setup(version=PACKAGE_INFO["__version__"])
225 changes: 225 additions & 0 deletions ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Copyright 2019, 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.

"""
The opentelemetry-ext-dbapi package allows tracing queries made by the
ibraries following Ptyhon Database API specification:
https://www.python.org/dev/peps/pep-0249/
"""

import logging
import typing

import wrapt

from opentelemetry.trace import SpanKind, Tracer
from opentelemetry.trace.status import Status, StatusCanonicalCode

logger = logging.getLogger(__name__)


def trace_integration(
tracer: Tracer,
connect_module: typing.Callable[..., any],
connect_method_name: str,
database_component: str,
database_type: str = "",
connection_attributes: typing.Dict = None,
):
"""Integrate with DB API library.
https://www.python.org/dev/peps/pep-0249/
Args:
tracer: The :class:`Tracer` to use.
connect_module: Module name where connect method is available.
connect_method_name: The connect method name.
database_component: Database driver name or database name "JDBI", "jdbc", "odbc", "postgreSQL".
database_type: The Database type. For any SQL database, "sql".
connection_attributes: Attribute names for database, port, host and user in Connection object.
"""

# pylint: disable=unused-argument
def wrap_connect(
wrapped: typing.Callable[..., any],
instance: typing.Any,
args: typing.Tuple[any, any],
kwargs: typing.Dict[any, any],
):
db_integration = DatabaseApiIntegration(
tracer,
database_component,
database_type=database_type,
connection_attributes=connection_attributes,
)
return db_integration.wrapped_connection(wrapped, args, kwargs)

try:
wrapt.wrap_function_wrapper(
connect_module, connect_method_name, wrap_connect
)
except Exception as ex: # pylint: disable=broad-except
logger.warning("Failed to integrate with DB API. %s", str(ex))


class DatabaseApiIntegration:
# pylint: disable=unused-argument
def __init__(
self,
tracer: Tracer,
database_component: str,
database_type: str = "sql",
connection_attributes=None,
):
if tracer is None:
raise ValueError("The tracer is not provided.")
self.connection_attributes = connection_attributes
if self.connection_attributes is None:
self.connection_attributes = {
"database": "database",
"port": "port",
"host": "host",
"user": "user",
}
self.tracer = tracer
self.database_component = database_component
self.database_type = database_type
self.connection_props = {}
self.span_attributes = {}
self.name = ""
self.database = ""

def wrapped_connection(
self,
connect_method: typing.Callable[..., any],
args: typing.Tuple[any, any],
kwargs: typing.Dict[any, any],
):
"""Add object proxy to connection object.
"""
connection = connect_method(*args, **kwargs)

for key, value in self.connection_attributes.items():
attribute = getattr(connection, value, None)
if attribute:
self.connection_props[key] = attribute
traced_connection = TracedConnection(connection, self)
return traced_connection


# pylint: disable=abstract-method
class TracedConnection(wrapt.ObjectProxy):

# pylint: disable=unused-argument
def __init__(
self,
connection,
db_api_integration: DatabaseApiIntegration,
*args,
**kwargs
):
wrapt.ObjectProxy.__init__(self, connection)
self._db_api_integration = db_api_integration

self._db_api_integration.name = (
self._db_api_integration.database_component
)
self._db_api_integration.database = self._db_api_integration.connection_props.get(
"database", ""
)
if self._db_api_integration.database:
self._db_api_integration.name += (
"." + self._db_api_integration.database
)
user = self._db_api_integration.connection_props.get("user")
if user is not None:
self._db_api_integration.span_attributes["db.user"] = user
host = self._db_api_integration.connection_props.get("host")
if host is not None:
self._db_api_integration.span_attributes["net.peer.name"] = host
port = self._db_api_integration.connection_props.get("port")
if port is not None:
self._db_api_integration.span_attributes["net.peer.port"] = port

def cursor(self, *args, **kwargs):
return TracedCursor(
self.__wrapped__.cursor(*args, **kwargs), self._db_api_integration
)


# pylint: disable=abstract-method
class TracedCursor(wrapt.ObjectProxy):

# pylint: disable=unused-argument
def __init__(
self,
cursor,
db_api_integration: DatabaseApiIntegration,
*args,
**kwargs
):
wrapt.ObjectProxy.__init__(self, cursor)
self._db_api_integration = db_api_integration

def execute(self, *args, **kwargs):
return self._traced_execution(
self.__wrapped__.execute, *args, **kwargs
)

def executemany(self, *args, **kwargs):
return self._traced_execution(
self.__wrapped__.executemany, *args, **kwargs
)

def callproc(self, *args, **kwargs):
return self._traced_execution(
self.__wrapped__.callproc, *args, **kwargs
)

def _traced_execution(
self,
query_method: typing.Callable[..., any],
*args: typing.Tuple[any, any],
**kwargs: typing.Dict[any, any]
):

statement = args[0] if args else ""
with self._db_api_integration.tracer.start_as_current_span(
self._db_api_integration.name, kind=SpanKind.CLIENT
) as span:
span.set_attribute(
"component", self._db_api_integration.database_component
)
span.set_attribute(
"db.type", self._db_api_integration.database_type
)
span.set_attribute(
"db.instance", self._db_api_integration.database
)
span.set_attribute("db.statement", statement)

for (
attribute_key,
attribute_value,
) in self._db_api_integration.span_attributes.items():
span.set_attribute(attribute_key, attribute_value)

if len(args) > 1:
span.set_attribute("db.statement.parameters", str(args[1]))

try:
result = query_method(*args, **kwargs)
span.set_status(Status(StatusCanonicalCode.OK))
return result
except Exception as ex: # pylint: disable=broad-except
span.set_status(Status(StatusCanonicalCode.UNKNOWN, str(ex)))
raise ex
15 changes: 15 additions & 0 deletions ext/opentelemetry-ext-dbapi/src/opentelemetry/ext/dbapi/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2019, 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.4.dev0"
Empty file.
Loading

0 comments on commit b01f7e8

Please sign in to comment.