From 395ede550bf065cb2f30e84a1003599ad8764bb4 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 10:20:46 +0100 Subject: [PATCH 01/14] 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 e69c6dbcb4..d25cb73739 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py @@ -125,7 +125,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 08e2f54c0e..60a1c9cdaa 100644 --- a/tox.ini +++ b/tox.ini @@ -126,8 +126,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 @@ -177,6 +177,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 @@ -295,7 +298,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}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] From 54aded7c2a1e1d2a8534a39a9ca922a7a3f60fb1 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 11:35:58 +0100 Subject: [PATCH 02/14] initial commit - copy of redis inc tests --- .../MANIFEST.in | 9 ++ .../README.rst | 24 ++++ .../setup.cfg | 56 +++++++++ .../setup.py | 89 ++++++++++++++ .../instrumentation/aioredis/__init__.py | 112 ++++++++++++++++++ .../instrumentation/aioredis/package.py | 16 +++ .../instrumentation/aioredis/util.py | 73 ++++++++++++ .../instrumentation/aioredis/version.py | 15 +++ .../tests/__init__.py | 13 ++ .../tests/test_redis.py | 82 +++++++++++++ tox.ini | 9 +- 11 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/MANIFEST.in create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/README.rst create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/setup.cfg create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/setup.py create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/package.py create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/util.py create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/version.py create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/tests/__init__.py create mode 100644 instrumentation/opentelemetry-instrumentation-aioredis/tests/test_redis.py diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-aioredis/MANIFEST.in new file mode 100644 index 0000000000..aed3e33273 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/MANIFEST.in @@ -0,0 +1,9 @@ +graft src +graft tests +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include CHANGELOG.md +include MANIFEST.in +include README.rst +include LICENSE diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/README.rst b/instrumentation/opentelemetry-instrumentation-aioredis/README.rst new file mode 100644 index 0000000000..cae4635613 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/README.rst @@ -0,0 +1,24 @@ +OpenTelemetry Redis Instrumentation +=================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-redis.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-redis/ + +This library allows tracing requests made by the Redis library. + +Installation +------------ + +:: + + pip install opentelemetry-instrumentation-redis + + +References +---------- + +* `OpenTelemetry Redis Instrumentation `_ +* `OpenTelemetry Project `_ +* `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/setup.cfg b/instrumentation/opentelemetry-instrumentation-aioredis/setup.cfg new file mode 100644 index 0000000000..4aea77df3e --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/setup.cfg @@ -0,0 +1,56 @@ +# 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-aioredis +description = OpenTelemetry aioredis instrumentation +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-aioredis +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.4.0.dev0 + opentelemetry-semantic-conventions == 0.23.dev0 + opentelemetry-instrumentation == 0.23.dev0 + wrapt >= 1.12.1 + +[options.extras_require] +test = + opentelemetry-test == 0.23.dev0 + opentelemetry-sdk == 1.4.0.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +opentelemetry_instrumentor = + aioredis = opentelemetry.instrumentation.aioredis:AioRedisInstrumentor diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/setup.py b/instrumentation/opentelemetry-instrumentation-aioredis/setup.py new file mode 100644 index 0000000000..bba16e97a7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/setup.py @@ -0,0 +1,89 @@ +# 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", "aioredis", "version.py" +) +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +PACKAGE_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "instrumentation", "aioredis", "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-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py new file mode 100644 index 0000000000..ccce6a12e5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py @@ -0,0 +1,112 @@ +# 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 `aioredis`_ to report Redis queries. + +There are two options for instrumenting code. The first option is to use the +``opentelemetry-instrumentation`` executable which will automatically +instrument your Redis client. The second is to programmatically enable +instrumentation via the following code: + +.. _aioredis: https://pypi.org/project/aioredis/ + +Usage +----- + +.. code:: python + + from opentelemetry.instrumentation.aioredis import AioRedisInstrumentor + import aioredis + + + # Instrument redis + AioRedisInstrumentor().instrument() + + # This will report a span with the default settings + client = redis.StrictRedis(host="localhost", port=6379) + client.get("my-key") + +API +--- +""" + +from typing import Collection + +import aioredis +from wrapt import wrap_function_wrapper + +from opentelemetry import trace +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.aioredis.package import _instruments +from opentelemetry.instrumentation.aioredis.util import ( + _format_command_args, +) +from opentelemetry.instrumentation.aioredis.version import __version__ +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.semconv.trace import SpanAttributes, NetTransportValues, DbSystemValues + +_DEFAULT_SERVICE = "redis" + +async def traced_execute(func, instance: aioredis.Redis, args, kwargs): + tracer = getattr(aioredis, "_opentelemetry_tracer") + query = _format_command_args(args) + name: str = "" + if len(args) > 0 and args[0]: + name = args[0].decode("utf-8") + else: + name = str(instance.db) + with tracer.start_as_current_span(name, kind=trace.SpanKind.CLIENT) as span: + if span.is_recording(): + span.set_attributes( + { + SpanAttributes.DB_SYSTEM: DbSystemValues.REDIS.value, + SpanAttributes.DB_STATEMENT: query, + SpanAttributes.DB_NAME: instance.db, + SpanAttributes.DB_REDIS_DATABASE_INDEX: instance.db, + } + ) + span.set_attribute("db.redis.args_length", len(args)) + if instance.address: + span.set_attributes( + { + SpanAttributes.NET_PEER_NAME: instance.address[0], + SpanAttributes.NET_PEER_PORT: instance.address[1], + SpanAttributes.NET_TRANSPORT: NetTransportValues.IP_TCP.value, + } + ) + return await func(*args, **kwargs) + + +class AioRedisInstrumentor(BaseInstrumentor): + """An instrumentor for aioredis + See `BaseInstrumentor` + """ + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + tracer_provider = kwargs.get("tracer_provider") + setattr( + aioredis, + "_opentelemetry_tracer", + trace.get_tracer( + __name__, __version__, tracer_provider=tracer_provider, + ), + ) + wrap_function_wrapper(aioredis, "Redis.execute", traced_execute) + + def _uninstrument(self, **kwargs): + unwrap(aioredis.Redis, "execute") diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/package.py b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/package.py new file mode 100644 index 0000000000..52040d7ee5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/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 = ("aioredis >= 1.3, < 2.0",) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/util.py b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/util.py new file mode 100644 index 0000000000..3419848eb0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/util.py @@ -0,0 +1,73 @@ +# 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. +# +""" +Some utils used by the redis integration +""" +from opentelemetry.semconv.trace import ( + DbSystemValues, + NetTransportValues, + SpanAttributes, +) + + +def _extract_conn_attributes(conn_kwargs): + """ Transform redis conn info into dict """ + attributes = { + SpanAttributes.DB_SYSTEM: DbSystemValues.REDIS.value, + } + db = conn_kwargs.get("db", 0) + attributes[SpanAttributes.DB_NAME] = db + attributes[SpanAttributes.DB_REDIS_DATABASE_INDEX] = db + try: + attributes[SpanAttributes.NET_PEER_NAME] = conn_kwargs.get( + "host", "localhost" + ) + attributes[SpanAttributes.NET_PEER_PORT] = conn_kwargs.get( + "port", 6379 + ) + attributes[ + SpanAttributes.NET_TRANSPORT + ] = NetTransportValues.IP_TCP.value + except KeyError: + attributes[SpanAttributes.NET_PEER_NAME] = conn_kwargs.get("path", "") + attributes[ + SpanAttributes.NET_TRANSPORT + ] = NetTransportValues.UNIX.value + + return attributes + + +def _format_command_args(args): + """Format command arguments and trim them as needed""" + value_max_len = 100 + value_too_long_mark = "..." + cmd_max_len = 1000 + length = 0 + out = [] + for arg in args: + cmd = str(arg) + + if len(cmd) > value_max_len: + cmd = cmd[:value_max_len] + value_too_long_mark + + if length + len(cmd) > cmd_max_len: + prefix = cmd[: cmd_max_len - length] + out.append("%s%s" % (prefix, value_too_long_mark)) + break + + out.append(cmd) + length += len(cmd) + + return " ".join(out) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/version.py b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/version.py new file mode 100644 index 0000000000..c829b95757 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/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.23.dev0" diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/__init__.py new file mode 100644 index 0000000000..b0a6f42841 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_redis.py new file mode 100644 index 0000000000..529c79c5d9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_redis.py @@ -0,0 +1,82 @@ +# 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. +from unittest import mock, IsolatedAsyncioTestCase + +import aioredis + +from opentelemetry.instrumentation.aioredis import AioRedisInstrumentor +from opentelemetry.test.test_base import TestBase +from opentelemetry.trace import SpanKind + +class TestRedis(TestBase, IsolatedAsyncioTestCase): + + async def test_span_properties(self): + redis_client = aioredis.Redis("redis://localhost") + AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + with mock.patch.object(redis_client, "_pool_or_conn", new=mock.AsyncMock(return_value="")): + await redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "GET") + self.assertEqual(span.kind, SpanKind.CLIENT) + + async def test_not_recording(self): + redis_client = aioredis.Redis("") + AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + tracer.return_value = mock_tracer + await redis_client.get("key") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + + async def test_instrument_uninstrument(self): + redis_client = aioredis.Redis("") + AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + await redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.memory_exporter.clear() + + # Test uninstrument + AioRedisInstrumentor().uninstrument() + + with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + await redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + self.memory_exporter.clear() + + # Test instrument again + AioRedisInstrumentor().instrument() + + with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + await redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) diff --git a/tox.ini b/tox.ini index 60a1c9cdaa..39b62c3931 100644 --- a/tox.ini +++ b/tox.ini @@ -133,6 +133,10 @@ envlist = py3{6,7,8,9}-test-instrumentation-redis pypy3-test-instrumentation-redis + ; opentelemetry-instrumentation-aioredis + py3{6,7,8,9}-test-instrumentation-aioredis + pypy3-test-instrumentation-aioredis + ; opentelemetry-instrumentation-celery py3{6,7,8,9}-test-instrumentation-celery pypy3-test-instrumentation-celery @@ -280,6 +284,8 @@ commands_pre = redis: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-redis[test] + aioredis: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aioredis[test] + requests: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-requests[test] starlette: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-starlette[test] @@ -338,7 +344,7 @@ commands = [testenv:lint] basepython: python3.9 -recreate = False +recreate = False deps = -c dev-requirements.txt flaky @@ -371,6 +377,7 @@ commands_pre = python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-celery[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis[test] + python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-aioredis[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-fastapi[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-jinja2[test] python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-logging[test] From c830d4b9d4dc50ebec572830cfa5d5f312ae9534 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 11:37:48 +0100 Subject: [PATCH 03/14] rename files. update readme --- .../README.rst | 12 ++++++------ .../tests/{test_redis.py => test_aioredis.py} | 0 2 files changed, 6 insertions(+), 6 deletions(-) rename instrumentation/opentelemetry-instrumentation-aioredis/tests/{test_redis.py => test_aioredis.py} (100%) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/README.rst b/instrumentation/opentelemetry-instrumentation-aioredis/README.rst index cae4635613..05e4244a58 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/README.rst +++ b/instrumentation/opentelemetry-instrumentation-aioredis/README.rst @@ -1,24 +1,24 @@ -OpenTelemetry Redis Instrumentation +OpenTelemetry AioRedis Instrumentation =================================== |pypi| -.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-redis.svg - :target: https://pypi.org/project/opentelemetry-instrumentation-redis/ +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aioredis.svg + :target: https://pypi.org/project/opentelemetry-instrumentation-aioredis/ -This library allows tracing requests made by the Redis library. +This library allows tracing requests made by the aioredis library. Installation ------------ :: - pip install opentelemetry-instrumentation-redis + pip install opentelemetry-instrumentation-aioredis References ---------- -* `OpenTelemetry Redis Instrumentation `_ +* `OpenTelemetry aioredis Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py similarity index 100% rename from instrumentation/opentelemetry-instrumentation-aioredis/tests/test_redis.py rename to instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py From 1672b83461c82d3340ce872a6cadc27a99ad8b42 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 11:57:17 +0100 Subject: [PATCH 04/14] Revert "add tests for async. fix black issues" This reverts commit 395ede550bf065cb2f30e84a1003599ad8764bb4. --- .../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, 12 insertions(+), 49 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 d25cb73739..e69c6dbcb4 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py @@ -125,10 +125,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/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 84904ad859..4a633687e6 100644 --- a/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py +++ b/instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py @@ -11,19 +11,13 @@ # 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): @@ -34,8 +28,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() @@ -45,25 +38,6 @@ 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() @@ -73,8 +47,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/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" diff --git a/tox.ini b/tox.ini index 39b62c3931..53349f3fb1 100644 --- a/tox.ini +++ b/tox.ini @@ -126,8 +126,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 + pypy3-test-instrumentation-sqlalchemy ; opentelemetry-instrumentation-redis py3{6,7,8,9}-test-instrumentation-redis @@ -181,9 +181,6 @@ 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 @@ -304,7 +301,7 @@ commands_pre = sklearn: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sklearn[test] - sqlalchemy{13,14}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] + sqlalchemy: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy[test] elasticsearch{2,5,6,7}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test] From 182f0f3a5b1bd2d5bc8f0a755071723790cf979b Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 11:58:12 +0100 Subject: [PATCH 05/14] run tox -e generate --- .../setup.py | 14 +++++++++-- .../instrumentation/aioredis/__init__.py | 17 ++++++++----- .../tests/test_aioredis.py | 24 +++++++++++++------ .../instrumentation/bootstrap_gen.py | 4 ++++ 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/setup.py b/instrumentation/opentelemetry-instrumentation-aioredis/setup.py index bba16e97a7..38aad1fa8c 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/setup.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/setup.py @@ -39,13 +39,23 @@ PACKAGE_INFO = {} VERSION_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "aioredis", "version.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "aioredis", + "version.py", ) with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) PACKAGE_FILENAME = os.path.join( - BASE_DIR, "src", "opentelemetry", "instrumentation", "aioredis", "package.py" + BASE_DIR, + "src", + "opentelemetry", + "instrumentation", + "aioredis", + "package.py", ) with open(PACKAGE_FILENAME) as f: exec(f.read(), PACKAGE_INFO) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py index ccce6a12e5..c6c39c87e4 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/src/opentelemetry/instrumentation/aioredis/__init__.py @@ -48,17 +48,20 @@ from wrapt import wrap_function_wrapper from opentelemetry import trace -from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.aioredis.package import _instruments -from opentelemetry.instrumentation.aioredis.util import ( - _format_command_args, -) +from opentelemetry.instrumentation.aioredis.util import _format_command_args from opentelemetry.instrumentation.aioredis.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap -from opentelemetry.semconv.trace import SpanAttributes, NetTransportValues, DbSystemValues +from opentelemetry.semconv.trace import ( + DbSystemValues, + NetTransportValues, + SpanAttributes, +) _DEFAULT_SERVICE = "redis" + async def traced_execute(func, instance: aioredis.Redis, args, kwargs): tracer = getattr(aioredis, "_opentelemetry_tracer") query = _format_command_args(args) @@ -67,7 +70,9 @@ async def traced_execute(func, instance: aioredis.Redis, args, kwargs): name = args[0].decode("utf-8") else: name = str(instance.db) - with tracer.start_as_current_span(name, kind=trace.SpanKind.CLIENT) as span: + with tracer.start_as_current_span( + name, kind=trace.SpanKind.CLIENT + ) as span: if span.is_recording(): span.set_attributes( { diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index 529c79c5d9..b84d18397d 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -11,7 +11,7 @@ # 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 mock, IsolatedAsyncioTestCase +from unittest import IsolatedAsyncioTestCase, mock import aioredis @@ -19,13 +19,15 @@ from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind -class TestRedis(TestBase, IsolatedAsyncioTestCase): +class TestRedis(TestBase, IsolatedAsyncioTestCase): async def test_span_properties(self): redis_client = aioredis.Redis("redis://localhost") AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - with mock.patch.object(redis_client, "_pool_or_conn", new=mock.AsyncMock(return_value="")): + with mock.patch.object( + redis_client, "_pool_or_conn", new=mock.AsyncMock(return_value="") + ): await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() @@ -43,7 +45,9 @@ async def test_not_recording(self): mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: - with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + with mock.patch.object( + redis_client, "_pool_or_conn", new_callable=mock.AsyncMock + ): tracer.return_value = mock_tracer await redis_client.get("key") self.assertFalse(mock_span.is_recording()) @@ -55,7 +59,9 @@ async def test_instrument_uninstrument(self): redis_client = aioredis.Redis("") AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + with mock.patch.object( + redis_client, "_pool_or_conn", new_callable=mock.AsyncMock + ): await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() @@ -65,7 +71,9 @@ async def test_instrument_uninstrument(self): # Test uninstrument AioRedisInstrumentor().uninstrument() - with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + with mock.patch.object( + redis_client, "_pool_or_conn", new_callable=mock.AsyncMock + ): await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() @@ -75,7 +83,9 @@ async def test_instrument_uninstrument(self): # Test instrument again AioRedisInstrumentor().instrument() - with mock.patch.object(redis_client, "_pool_or_conn", new_callable=mock.AsyncMock): + with mock.patch.object( + redis_client, "_pool_or_conn", new_callable=mock.AsyncMock + ): await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py index b49f40905f..23a5301b57 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py @@ -24,6 +24,10 @@ "library": "aiopg >= 0.13.0, < 1.3.0", "instrumentation": "opentelemetry-instrumentation-aiopg==0.23.dev0", }, + "aioredis": { + "library": "aioredis >= 1.3, < 2.0", + "instrumentation": "opentelemetry-instrumentation-aioredis==0.23.dev0", + }, "asgiref": { "library": "asgiref ~= 3.0", "instrumentation": "opentelemetry-instrumentation-asgi==0.23.dev0", From 2c6bd411b0b1003ae903efcb2bb1d5f42e38ba08 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 14:01:12 +0100 Subject: [PATCH 06/14] fix test directory for aioredis --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 53349f3fb1..ec75e8108a 100644 --- a/tox.ini +++ b/tox.ini @@ -212,6 +212,7 @@ changedir = test-instrumentation-pymysql: instrumentation/opentelemetry-instrumentation-pymysql/tests test-instrumentation-pyramid: instrumentation/opentelemetry-instrumentation-pyramid/tests test-instrumentation-redis: instrumentation/opentelemetry-instrumentation-redis/tests + test-instrumentation-aioredis: instrumentation/opentelemetry-instrumentation-aioredis/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 From ff589612b834023cf6265b4021c00bd9b6a4f1fa Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 14:37:26 +0100 Subject: [PATCH 07/14] address pylint error --- .../tests/test_aioredis.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index b84d18397d..f07e608a17 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -11,7 +11,10 @@ # 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 +from unittest import ( # pylint: disable=no-name-in-module + IsolatedAsyncioTestCase, + mock, +) import aioredis From d610115bbde8d86fa765dc956e85f1c37d50bdbc Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 15:06:02 +0100 Subject: [PATCH 08/14] lint, tests and generate --- .../README.rst | 2 +- .../tests/test_aioredis.py | 153 ++++++++++-------- 2 files changed, 85 insertions(+), 70 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/README.rst b/instrumentation/opentelemetry-instrumentation-aioredis/README.rst index 05e4244a58..63b28917ac 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/README.rst +++ b/instrumentation/opentelemetry-instrumentation-aioredis/README.rst @@ -1,5 +1,5 @@ OpenTelemetry AioRedis Instrumentation -=================================== +====================================== |pypi| diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index f07e608a17..b5ab9b607b 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -11,10 +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 ( # pylint: disable=no-name-in-module - IsolatedAsyncioTestCase, - mock, -) +import asyncio +from unittest import mock import aioredis @@ -23,73 +21,90 @@ from opentelemetry.trace import SpanKind -class TestRedis(TestBase, IsolatedAsyncioTestCase): +class TestRedis(TestBase): async def test_span_properties(self): - redis_client = aioredis.Redis("redis://localhost") - AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - - with mock.patch.object( - redis_client, "_pool_or_conn", new=mock.AsyncMock(return_value="") - ): - await redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - span = spans[0] - self.assertEqual(span.name, "GET") - self.assertEqual(span.kind, SpanKind.CLIENT) - - async def test_not_recording(self): - redis_client = aioredis.Redis("") - AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - - mock_tracer = mock.Mock() - mock_span = mock.Mock() - mock_span.is_recording.return_value = False - mock_tracer.start_span.return_value = mock_span - with mock.patch("opentelemetry.trace.get_tracer") as tracer: + async def run(): + redis_client = aioredis.Redis("redis://localhost") + AioRedisInstrumentor().instrument( + tracer_provider=self.tracer_provider + ) + + with mock.patch.object( + redis_client, + "_pool_or_conn", + new=mock.AsyncMock(return_value=""), + ): + await redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + span = spans[0] + self.assertEqual(span.name, "GET") + self.assertEqual(span.kind, SpanKind.CLIENT) + + asyncio.get_event_loop().run_until_complete(run()) + + def test_not_recording(self): + async def run(): + redis_client = aioredis.Redis("") + AioRedisInstrumentor().instrument( + tracer_provider=self.tracer_provider + ) + + mock_tracer = mock.Mock() + mock_span = mock.Mock() + mock_span.is_recording.return_value = False + mock_tracer.start_span.return_value = mock_span + with mock.patch("opentelemetry.trace.get_tracer") as tracer: + with mock.patch.object( + redis_client, "_pool_or_conn", new_callable=mock.AsyncMock + ): + tracer.return_value = mock_tracer + await redis_client.get("key") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) + + asyncio.get_event_loop().run_until_complete(run()) + + def test_instrument_uninstrument(self): + async def run(): + redis_client = aioredis.Redis("") + AioRedisInstrumentor().instrument( + tracer_provider=self.tracer_provider + ) + + with mock.patch.object( + redis_client, "_pool_or_conn", new_callable=mock.AsyncMock + ): + await redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + self.memory_exporter.clear() + + # Test uninstrument + AioRedisInstrumentor().uninstrument() + with mock.patch.object( redis_client, "_pool_or_conn", new_callable=mock.AsyncMock ): - tracer.return_value = mock_tracer await redis_client.get("key") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) - - async def test_instrument_uninstrument(self): - redis_client = aioredis.Redis("") - AioRedisInstrumentor().instrument(tracer_provider=self.tracer_provider) - - with mock.patch.object( - redis_client, "_pool_or_conn", new_callable=mock.AsyncMock - ): - await redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) - self.memory_exporter.clear() - - # Test uninstrument - AioRedisInstrumentor().uninstrument() - - with mock.patch.object( - redis_client, "_pool_or_conn", new_callable=mock.AsyncMock - ): - await redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 0) - self.memory_exporter.clear() - - # Test instrument again - AioRedisInstrumentor().instrument() - - with mock.patch.object( - redis_client, "_pool_or_conn", new_callable=mock.AsyncMock - ): - await redis_client.get("key") - - spans = self.memory_exporter.get_finished_spans() - self.assertEqual(len(spans), 1) + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 0) + self.memory_exporter.clear() + + # Test instrument again + AioRedisInstrumentor().instrument() + + with mock.patch.object( + redis_client, "_pool_or_conn", new_callable=mock.AsyncMock + ): + await redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + asyncio.get_event_loop().run_until_complete(run()) From 179c227ad5e0ade0d0d98ce655f051cde5a1a39c Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 15:37:25 +0100 Subject: [PATCH 09/14] add AsyncMock support for python <3.8 --- .../tests/test_aioredis.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index b5ab9b607b..6701f390b8 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio +import sys from unittest import mock import aioredis @@ -20,6 +21,15 @@ from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind +if sys.version_info > (3, 7, 0): + from unittest.mock import AsyncMock +else: + from unittest.mock import MagicMock + + class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + return super(AsyncMock, self).__call__(*args, **kwargs) + class TestRedis(TestBase): async def test_span_properties(self): From 660cfd8c0ff99b9a0aaab058543b8c23cddfbf5c Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 16:00:50 +0100 Subject: [PATCH 10/14] fix pylint for AsyncMock --- .../tests/test_aioredis.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index 6701f390b8..4c5cdac655 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -28,7 +28,11 @@ class AsyncMock(MagicMock): async def __call__(self, *args, **kwargs): - return super(AsyncMock, self).__call__(*args, **kwargs) + return super( + AsyncMock, self + ).__call__( # pylint: disable=useless-super-delegation + *args, **kwargs + ) class TestRedis(TestBase): From 82ff5df783c26c99f9392ebab1afa30a030fedcd Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 16:10:00 +0100 Subject: [PATCH 11/14] address pylint error --- .../tests/test_aioredis.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index 4c5cdac655..af2c32c58b 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -27,12 +27,10 @@ from unittest.mock import MagicMock class AsyncMock(MagicMock): + + # pylint: disable=useless-super-delegation async def __call__(self, *args, **kwargs): - return super( - AsyncMock, self - ).__call__( # pylint: disable=useless-super-delegation - *args, **kwargs - ) + return super(AsyncMock, self).__call__(*args, **kwargs) class TestRedis(TestBase): From 506faeb6ee4ce19e4385c124f3b2823fe0acc917 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 16:55:22 +0100 Subject: [PATCH 12/14] use custom Mock for cross python version compat --- .../tests/test_aioredis.py | 68 +++++++------------ 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index af2c32c58b..d78db5e942 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio -import sys from unittest import mock import aioredis @@ -21,33 +20,30 @@ from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind -if sys.version_info > (3, 7, 0): - from unittest.mock import AsyncMock -else: - from unittest.mock import MagicMock - class AsyncMock(MagicMock): +class MockPool: + return_value = None - # pylint: disable=useless-super-delegation - async def __call__(self, *args, **kwargs): - return super(AsyncMock, self).__call__(*args, **kwargs) + async def execute(self, *args, **kwargs): + return self.return_value + + @property + def db(self): + return 0 + + @property + def address(self): + return ("127.0.0.1", 6376) class TestRedis(TestBase): - async def test_span_properties(self): + def test_span_properties(self): async def run(): - redis_client = aioredis.Redis("redis://localhost") + redis_client = aioredis.Redis(MockPool()) AioRedisInstrumentor().instrument( tracer_provider=self.tracer_provider ) - - with mock.patch.object( - redis_client, - "_pool_or_conn", - new=mock.AsyncMock(return_value=""), - ): - await redis_client.get("key") - + await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) span = spans[0] @@ -58,7 +54,7 @@ async def run(): def test_not_recording(self): async def run(): - redis_client = aioredis.Redis("") + redis_client = aioredis.Redis(MockPool()) AioRedisInstrumentor().instrument( tracer_provider=self.tracer_provider ) @@ -68,29 +64,23 @@ async def run(): mock_span.is_recording.return_value = False mock_tracer.start_span.return_value = mock_span with mock.patch("opentelemetry.trace.get_tracer") as tracer: - with mock.patch.object( - redis_client, "_pool_or_conn", new_callable=mock.AsyncMock - ): - tracer.return_value = mock_tracer - await redis_client.get("key") - self.assertFalse(mock_span.is_recording()) - self.assertTrue(mock_span.is_recording.called) - self.assertFalse(mock_span.set_attribute.called) - self.assertFalse(mock_span.set_status.called) + tracer.return_value = mock_tracer + await redis_client.get("key") + self.assertFalse(mock_span.is_recording()) + self.assertTrue(mock_span.is_recording.called) + self.assertFalse(mock_span.set_attribute.called) + self.assertFalse(mock_span.set_status.called) asyncio.get_event_loop().run_until_complete(run()) def test_instrument_uninstrument(self): async def run(): - redis_client = aioredis.Redis("") + redis_client = aioredis.Redis(MockPool()) AioRedisInstrumentor().instrument( tracer_provider=self.tracer_provider ) - with mock.patch.object( - redis_client, "_pool_or_conn", new_callable=mock.AsyncMock - ): - await redis_client.get("key") + await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) @@ -99,10 +89,7 @@ async def run(): # Test uninstrument AioRedisInstrumentor().uninstrument() - with mock.patch.object( - redis_client, "_pool_or_conn", new_callable=mock.AsyncMock - ): - await redis_client.get("key") + await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 0) @@ -111,10 +98,7 @@ async def run(): # Test instrument again AioRedisInstrumentor().instrument() - with mock.patch.object( - redis_client, "_pool_or_conn", new_callable=mock.AsyncMock - ): - await redis_client.get("key") + await redis_client.get("key") spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 1) From dcce039ebe637f361d5bf52053bb93005ded07c8 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 17:12:06 +0100 Subject: [PATCH 13/14] address lint issues --- .../tests/test_aioredis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py index d78db5e942..d47a00be29 100644 --- a/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py +++ b/instrumentation/opentelemetry-instrumentation-aioredis/tests/test_aioredis.py @@ -28,7 +28,7 @@ async def execute(self, *args, **kwargs): return self.return_value @property - def db(self): + def db(self): # pylint: disable=invalid-name return 0 @property From 5eb4ef2249d4bd10e40ec9af45c701327d1047f3 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Wed, 7 Jul 2021 18:00:32 +0100 Subject: [PATCH 14/14] fix changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2a12f78f..c5fa8d1859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation ([#461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/461)) +- `opentelemetry-instrumentation-aioredis` Add `aioredis` instrumentation + ([#569](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/569)) ## [0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01