Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Porting redis instrumentation from contrib repo #595

Merged
merged 39 commits into from
Apr 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f91ce7c
porting redis instrumentation
Apr 17, 2020
88ea6f1
fix lint and docker-tests
Apr 17, 2020
8799497
Merge remote-tracking branch 'origin/master' into instrumentation-redis
Apr 20, 2020
4157cbf
small cleanup
Apr 20, 2020
1e48c7d
improving documentation
Apr 20, 2020
d0af1ec
expect a tracer_provider instead of a tracer
Apr 20, 2020
6809d9a
fixing tox.ini
Apr 20, 2020
64e1d72
fixing tox file
Apr 20, 2020
2e71d4f
removing reload, it breaks checks for Span types
Apr 20, 2020
a98fb47
remove reload
Apr 21, 2020
43ae8b0
Merge remote-tracking branch 'origin/master' into instrumentation-redis
Apr 22, 2020
8933c0b
cleaning up tests
Apr 22, 2020
9b7e311
adding comment to remember to update how the tracer is set
Apr 22, 2020
5e8172d
Merge remote-tracking branch 'origin/master' into instrumentation-redis
Apr 22, 2020
a33287b
updating code to use instrument/uninstrument interface
Apr 22, 2020
5f40067
adding check for redis, refactoring and improving error handling
Apr 23, 2020
2bcadf5
clean unused import
Apr 23, 2020
65c6cb3
adding changelog
Apr 23, 2020
3a1d1dd
Merge remote-tracking branch 'origin/master' into instrumentation-redis
Apr 23, 2020
5fd98fe
update doc
Apr 23, 2020
ffab5f9
Update ext/opentelemetry-instrumentation-redis/src/opentelemetry/inst…
Apr 24, 2020
039ad13
PyMySQL Integration (#504)
lzchen Apr 24, 2020
75a9bf4
review feedback
Apr 24, 2020
28e6d7c
review feedback
Apr 24, 2020
9913c1c
Merge remote-tracking branch 'origin/master' into instrumentation-redis
Apr 24, 2020
1b978a5
Apply suggestions from code review
Apr 24, 2020
5d74317
adding version for redis
Apr 24, 2020
d843cad
more review feedback changes
Apr 24, 2020
e064dc1
rename from instrumentation to ext
Apr 24, 2020
2712732
Update ext/opentelemetry-ext-redis/src/opentelemetry/instrumentation/…
Apr 24, 2020
8907032
fixing exception in check_availability
Apr 24, 2020
5ee71de
finish renaming, remove patch.py
Apr 24, 2020
2faca48
fix lint
Apr 24, 2020
f372d1e
Apply suggestions from code review
Apr 24, 2020
87eceb6
sdk: span parents are now always spancontext (#548)
toumorokoshi Apr 25, 2020
a10916b
fix lint
Apr 26, 2020
0757069
refactor tests
Apr 26, 2020
e4d9084
set attributes that are available
Apr 26, 2020
b4d810e
Merge branch 'master' into instrumentation-redis
toumorokoshi Apr 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ multi_line_output=3
skip=target
skip_glob=**/gen/*,.venv*/*,venv*/*
known_first_party=opentelemetry,opentelemetry_example_app
known_third_party=psutil,pytest
known_third_party=psutil,pytest,redis,redis_opentracing
7 changes: 7 additions & 0 deletions docs/ext/redis/redis.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
OpenTelemetry Redis Instrumentation
===================================

.. automodule:: opentelemetry.ext.redis
:members:
:undoc-members:
:show-inheritance:
107 changes: 57 additions & 50 deletions ext/opentelemetry-ext-docker-tests/tests/check_availability.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import mysql.connector
import psycopg2
import pymongo
import redis

MONGODB_COLLECTION_NAME = "test"
MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests")
Expand All @@ -33,76 +34,82 @@
POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST", "testpassword")
POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432"))
POSTGRES_USER = os.getenv("POSTGRESQL_HOST", "testuser")
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379"))
RETRY_COUNT = 5
RETRY_INTERVAL = 5 # Seconds

logger = logging.getLogger(__name__)


def retryable(func):
def wrapper():
# Try to connect to DB
for i in range(RETRY_COUNT):
codeboten marked this conversation as resolved.
Show resolved Hide resolved
try:
func()
return
except Exception as ex: # pylint: disable=broad-except
logger.error(
"waiting for %s, retry %d/%d [%s]",
func.__name__,
i + 1,
RETRY_COUNT,
ex,
)
time.sleep(RETRY_INTERVAL)
raise Exception("waiting for {} failed".format(func.__name__))

return wrapper


@retryable
def check_pymongo_connection():
codeboten marked this conversation as resolved.
Show resolved Hide resolved
# Try to connect to DB
for i in range(RETRY_COUNT):
try:
client = pymongo.MongoClient(
MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000
)
db = client[MONGODB_DB_NAME]
collection = db[MONGODB_COLLECTION_NAME]
collection.find_one()
client.close()
break
except Exception as ex:
if i == RETRY_COUNT - 1:
raise (ex)
logger.exception(ex)
time.sleep(RETRY_INTERVAL)
client = pymongo.MongoClient(
MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000
)
db = client[MONGODB_DB_NAME]
collection = db[MONGODB_COLLECTION_NAME]
collection.find_one()
client.close()


@retryable
def check_mysql_connection():
# Try to connect to DB
for i in range(RETRY_COUNT):
try:
connection = mysql.connector.connect(
user=MYSQL_USER,
password=MYSQL_PASSWORD,
host=MYSQL_HOST,
port=MYSQL_PORT,
database=MYSQL_DB_NAME,
)
connection.close()
break
except Exception as ex:
if i == RETRY_COUNT - 1:
raise (ex)
logger.exception(ex)
time.sleep(RETRY_INTERVAL)
connection = mysql.connector.connect(
user=MYSQL_USER,
password=MYSQL_PASSWORD,
host=MYSQL_HOST,
port=MYSQL_PORT,
database=MYSQL_DB_NAME,
)
connection.close()


@retryable
def check_postgres_connection():
# Try to connect to DB
for i in range(RETRY_COUNT):
try:
connection = psycopg2.connect(
dbname=POSTGRES_DB_NAME,
user=POSTGRES_USER,
password=POSTGRES_PASSWORD,
host=POSTGRES_HOST,
port=POSTGRES_PORT,
)
connection.close()
break
except Exception as ex:
if i == RETRY_COUNT - 1:
raise (ex)
logger.exception(ex)
time.sleep(RETRY_INTERVAL)
connection = psycopg2.connect(
dbname=POSTGRES_DB_NAME,
user=POSTGRES_USER,
password=POSTGRES_PASSWORD,
host=POSTGRES_HOST,
port=POSTGRES_PORT,
)
connection.close()


@retryable
def check_redis_connection():
connection = redis.Redis(host=REDIS_HOST, port=REDIS_PORT)
connection.hgetall("*")


def check_docker_services_availability():
# Check if Docker services accept connections
check_pymongo_connection()
check_mysql_connection()
check_postgres_connection()
check_redis_connection()


check_docker_services_availability()
ocelotl marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ services:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: opentelemetry-tests
otredis:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it is a really good approach to keep the tests of the different frameworks under the same test. What about if I only want to test redis?
Is there a technical limitation to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no technical limitation. it's not the path currently but we can split them if it makes more sense (i suspect as we have more integration tests, we will need to do this to run tests in parallel)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides parallel tests, there is also the case when you only want to run a specific test without downloading and starting the other containers that you don't need.
This could be a little bit complicated to set up, so we can ignore and handle in a follow up PR.

image: redis:4.0-alpine
ports:
- "127.0.0.1:6379:6379"

Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import redis

from opentelemetry import trace
from opentelemetry.ext.redis import RedisInstrumentor
from opentelemetry.test.test_base import TestBase


class TestRedisInstrument(TestBase):

test_service = "redis"

def setUp(self):
super().setUp()
codeboten marked this conversation as resolved.
Show resolved Hide resolved
self.redis_client = redis.Redis(port=6379)
self.redis_client.flushall()
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to call instrument/uninstrument once per test instead of once for the class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this way its not instrumenting the call to flushall which resets the redis data after each test.


def tearDown(self):
super().tearDown()
RedisInstrumentor().uninstrument()

def _check_span(self, span):
self.assertEqual(span.attributes["service"], self.test_service)
self.assertEqual(span.name, "redis.command")
self.assertIs(
span.status.canonical_code, trace.status.StatusCanonicalCode.OK
)
self.assertEqual(span.attributes.get("db.instance"), 0)
self.assertEqual(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth refactoring some of these assertions into a utility method? there's a lot of identical code with regards to db.instance / url.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, refactored.

span.attributes.get("db.url"), "redis://localhost:6379"
)

def test_long_command(self):
self.redis_client.mget(*range(1000))

spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 1)
span = spans[0]
self._check_span(span)
self.assertTrue(
span.attributes.get("db.statement").startswith("MGET 0 1 2 3")
)
self.assertTrue(span.attributes.get("db.statement").endswith("..."))

def test_basics(self):
self.assertIsNone(self.redis_client.get("cheese"))
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 1)
span = spans[0]
self._check_span(span)
self.assertEqual(span.attributes.get("db.statement"), "GET cheese")
self.assertEqual(span.attributes.get("redis.args_length"), 2)

def test_pipeline_traced(self):
with self.redis_client.pipeline(transaction=False) as pipeline:
pipeline.set("blah", 32)
pipeline.rpush("foo", "éé")
pipeline.hgetall("xxx")
pipeline.execute()

spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 1)
span = spans[0]
self._check_span(span)
self.assertEqual(
span.attributes.get("db.statement"),
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
)
self.assertEqual(span.attributes.get("redis.pipeline_length"), 3)

def test_pipeline_immediate(self):
with self.redis_client.pipeline() as pipeline:
pipeline.set("a", 1)
pipeline.immediate_execute_command("SET", "b", 2)
pipeline.execute()

spans = self.memory_exporter.get_finished_spans()
# expecting two separate spans here, rather than a
# single span for the whole pipeline
self.assertEqual(len(spans), 2)
span = spans[0]
self._check_span(span)
self.assertEqual(span.attributes.get("db.statement"), "SET b 2")

def test_parent(self):
"""Ensure OpenTelemetry works with redis."""
ot_tracer = trace.get_tracer("redis_svc")

with ot_tracer.start_as_current_span("redis_get"):
self.assertIsNone(self.redis_client.get("cheese"))

spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 2)
child_span, parent_span = spans[0], spans[1]

# confirm the parenting
self.assertIsNone(parent_span.parent)
self.assertIs(child_span.parent, parent_span.get_context())

self.assertEqual(parent_span.name, "redis_get")
self.assertEqual(parent_span.instrumentation_info.name, "redis_svc")

self.assertEqual(
child_span.attributes.get("service"), self.test_service
)
self.assertEqual(child_span.name, "redis.command")
5 changes: 5 additions & 0 deletions ext/opentelemetry-ext-redis/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## Unreleased

- Initial release
9 changes: 9 additions & 0 deletions ext/opentelemetry-ext-redis/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions ext/opentelemetry-ext-redis/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
OpenTelemetry Redis Instrumentation
===================================

|pypi|

.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-redis.svg
:target: https://pypi.org/project/opentelemetry-ext-redis/

This library allows tracing requests made by the Redis library.

Installation
------------

::

pip install opentelemetry-ext-redis


References
----------

* `OpenTelemetry Redis Instrumentation <https://opentelemetry-python.readthedocs.io/en/latest/ext/opentelemetry-ext-redis/opentelemetry-ext-redis.html>`_
* `OpenTelemetry Project <https://opentelemetry.io/>`_
58 changes: 58 additions & 0 deletions ext/opentelemetry-ext-redis/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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-ext-redis
description = Redis tracing for OpenTelemetry
long_description = file: README.rst
long_description_content_type = text/x-rst
author = OpenTelemetry Authors
author_email = [email protected]
url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-redis
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.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8

[options]
python_requires = >=3.4
package_dir=
=src
packages=find_namespace:
install_requires =
opentelemetry-api == 0.7dev0
opentelemetry-auto-instrumentation == 0.7dev0
redis >= 2.6
wrapt >= 1.12.1

[options.extras_require]
test =
opentelemetry-test == 0.7.dev0
opentelemetry-sdk == 0.7dev0

[options.packages.find]
where = src

[options.entry_points]
opentelemetry_instrumentor =
redis = opentelemetry.ext.redis:RedisInstrumentor
Loading