Skip to content

Commit a832bb0

Browse files
alrexmauriciovasquezbernallzchenocelotlc24t
authored
redis: Porting redis instrumentation from contrib repo (#595)
Porting the existing redis instrumentation from the contrib repo to using the OpenTelemetry API and the OpenTelemetry Auto-instrumentation Instrumentor interface. Similiar to the sqlalchemy PR, the main thing that will need updating is to remove the patch/unpatch methods once the instrumentor interface changes have been merged. This is replacing open-telemetry/opentelemetry-python-contrib#21 Co-authored-by: Mauricio Vásquez <[email protected]> Co-authored-by: Leighton Chen <[email protected]> Co-authored-by: Diego Hurtado <[email protected]> Co-authored-by: Chris Kleinknecht <[email protected]>
1 parent 8f0e584 commit a832bb0

File tree

16 files changed

+623
-53
lines changed

16 files changed

+623
-53
lines changed

.isort.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ multi_line_output=3
1515
skip=target
1616
skip_glob=**/gen/*,.venv*/*,venv*/*
1717
known_first_party=opentelemetry,opentelemetry_example_app
18-
known_third_party=psutil,pytest
18+
known_third_party=psutil,pytest,redis,redis_opentracing

docs/ext/redis/redis.rst

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OpenTelemetry Redis Instrumentation
2+
===================================
3+
4+
.. automodule:: opentelemetry.ext.redis
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

ext/opentelemetry-ext-docker-tests/tests/check_availability.py

+57-50
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import mysql.connector
1919
import psycopg2
2020
import pymongo
21+
import redis
2122

2223
MONGODB_COLLECTION_NAME = "test"
2324
MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests")
@@ -33,76 +34,82 @@
3334
POSTGRES_PASSWORD = os.getenv("POSTGRESQL_HOST", "testpassword")
3435
POSTGRES_PORT = int(os.getenv("POSTGRESQL_PORT", "5432"))
3536
POSTGRES_USER = os.getenv("POSTGRESQL_HOST", "testuser")
37+
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
38+
REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379"))
3639
RETRY_COUNT = 5
3740
RETRY_INTERVAL = 5 # Seconds
3841

3942
logger = logging.getLogger(__name__)
4043

4144

45+
def retryable(func):
46+
def wrapper():
47+
# Try to connect to DB
48+
for i in range(RETRY_COUNT):
49+
try:
50+
func()
51+
return
52+
except Exception as ex: # pylint: disable=broad-except
53+
logger.error(
54+
"waiting for %s, retry %d/%d [%s]",
55+
func.__name__,
56+
i + 1,
57+
RETRY_COUNT,
58+
ex,
59+
)
60+
time.sleep(RETRY_INTERVAL)
61+
raise Exception("waiting for {} failed".format(func.__name__))
62+
63+
return wrapper
64+
65+
66+
@retryable
4267
def check_pymongo_connection():
43-
# Try to connect to DB
44-
for i in range(RETRY_COUNT):
45-
try:
46-
client = pymongo.MongoClient(
47-
MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000
48-
)
49-
db = client[MONGODB_DB_NAME]
50-
collection = db[MONGODB_COLLECTION_NAME]
51-
collection.find_one()
52-
client.close()
53-
break
54-
except Exception as ex:
55-
if i == RETRY_COUNT - 1:
56-
raise (ex)
57-
logger.exception(ex)
58-
time.sleep(RETRY_INTERVAL)
68+
client = pymongo.MongoClient(
69+
MONGODB_HOST, MONGODB_PORT, serverSelectionTimeoutMS=2000
70+
)
71+
db = client[MONGODB_DB_NAME]
72+
collection = db[MONGODB_COLLECTION_NAME]
73+
collection.find_one()
74+
client.close()
5975

6076

77+
@retryable
6178
def check_mysql_connection():
62-
# Try to connect to DB
63-
for i in range(RETRY_COUNT):
64-
try:
65-
connection = mysql.connector.connect(
66-
user=MYSQL_USER,
67-
password=MYSQL_PASSWORD,
68-
host=MYSQL_HOST,
69-
port=MYSQL_PORT,
70-
database=MYSQL_DB_NAME,
71-
)
72-
connection.close()
73-
break
74-
except Exception as ex:
75-
if i == RETRY_COUNT - 1:
76-
raise (ex)
77-
logger.exception(ex)
78-
time.sleep(RETRY_INTERVAL)
79+
connection = mysql.connector.connect(
80+
user=MYSQL_USER,
81+
password=MYSQL_PASSWORD,
82+
host=MYSQL_HOST,
83+
port=MYSQL_PORT,
84+
database=MYSQL_DB_NAME,
85+
)
86+
connection.close()
7987

8088

89+
@retryable
8190
def check_postgres_connection():
82-
# Try to connect to DB
83-
for i in range(RETRY_COUNT):
84-
try:
85-
connection = psycopg2.connect(
86-
dbname=POSTGRES_DB_NAME,
87-
user=POSTGRES_USER,
88-
password=POSTGRES_PASSWORD,
89-
host=POSTGRES_HOST,
90-
port=POSTGRES_PORT,
91-
)
92-
connection.close()
93-
break
94-
except Exception as ex:
95-
if i == RETRY_COUNT - 1:
96-
raise (ex)
97-
logger.exception(ex)
98-
time.sleep(RETRY_INTERVAL)
91+
connection = psycopg2.connect(
92+
dbname=POSTGRES_DB_NAME,
93+
user=POSTGRES_USER,
94+
password=POSTGRES_PASSWORD,
95+
host=POSTGRES_HOST,
96+
port=POSTGRES_PORT,
97+
)
98+
connection.close()
99+
100+
101+
@retryable
102+
def check_redis_connection():
103+
connection = redis.Redis(host=REDIS_HOST, port=REDIS_PORT)
104+
connection.hgetall("*")
99105

100106

101107
def check_docker_services_availability():
102108
# Check if Docker services accept connections
103109
check_pymongo_connection()
104110
check_mysql_connection()
105111
check_postgres_connection()
112+
check_redis_connection()
106113

107114

108115
check_docker_services_availability()

ext/opentelemetry-ext-docker-tests/tests/docker-compose.yml

+4
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ services:
2323
POSTGRES_USER: testuser
2424
POSTGRES_PASSWORD: testpassword
2525
POSTGRES_DB: opentelemetry-tests
26+
otredis:
27+
image: redis:4.0-alpine
28+
ports:
29+
- "127.0.0.1:6379:6379"
2630

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import redis
16+
17+
from opentelemetry import trace
18+
from opentelemetry.ext.redis import RedisInstrumentor
19+
from opentelemetry.test.test_base import TestBase
20+
21+
22+
class TestRedisInstrument(TestBase):
23+
24+
test_service = "redis"
25+
26+
def setUp(self):
27+
super().setUp()
28+
self.redis_client = redis.Redis(port=6379)
29+
self.redis_client.flushall()
30+
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
31+
32+
def tearDown(self):
33+
super().tearDown()
34+
RedisInstrumentor().uninstrument()
35+
36+
def _check_span(self, span):
37+
self.assertEqual(span.attributes["service"], self.test_service)
38+
self.assertEqual(span.name, "redis.command")
39+
self.assertIs(
40+
span.status.canonical_code, trace.status.StatusCanonicalCode.OK
41+
)
42+
self.assertEqual(span.attributes.get("db.instance"), 0)
43+
self.assertEqual(
44+
span.attributes.get("db.url"), "redis://localhost:6379"
45+
)
46+
47+
def test_long_command(self):
48+
self.redis_client.mget(*range(1000))
49+
50+
spans = self.memory_exporter.get_finished_spans()
51+
self.assertEqual(len(spans), 1)
52+
span = spans[0]
53+
self._check_span(span)
54+
self.assertTrue(
55+
span.attributes.get("db.statement").startswith("MGET 0 1 2 3")
56+
)
57+
self.assertTrue(span.attributes.get("db.statement").endswith("..."))
58+
59+
def test_basics(self):
60+
self.assertIsNone(self.redis_client.get("cheese"))
61+
spans = self.memory_exporter.get_finished_spans()
62+
self.assertEqual(len(spans), 1)
63+
span = spans[0]
64+
self._check_span(span)
65+
self.assertEqual(span.attributes.get("db.statement"), "GET cheese")
66+
self.assertEqual(span.attributes.get("redis.args_length"), 2)
67+
68+
def test_pipeline_traced(self):
69+
with self.redis_client.pipeline(transaction=False) as pipeline:
70+
pipeline.set("blah", 32)
71+
pipeline.rpush("foo", "éé")
72+
pipeline.hgetall("xxx")
73+
pipeline.execute()
74+
75+
spans = self.memory_exporter.get_finished_spans()
76+
self.assertEqual(len(spans), 1)
77+
span = spans[0]
78+
self._check_span(span)
79+
self.assertEqual(
80+
span.attributes.get("db.statement"),
81+
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
82+
)
83+
self.assertEqual(span.attributes.get("redis.pipeline_length"), 3)
84+
85+
def test_pipeline_immediate(self):
86+
with self.redis_client.pipeline() as pipeline:
87+
pipeline.set("a", 1)
88+
pipeline.immediate_execute_command("SET", "b", 2)
89+
pipeline.execute()
90+
91+
spans = self.memory_exporter.get_finished_spans()
92+
# expecting two separate spans here, rather than a
93+
# single span for the whole pipeline
94+
self.assertEqual(len(spans), 2)
95+
span = spans[0]
96+
self._check_span(span)
97+
self.assertEqual(span.attributes.get("db.statement"), "SET b 2")
98+
99+
def test_parent(self):
100+
"""Ensure OpenTelemetry works with redis."""
101+
ot_tracer = trace.get_tracer("redis_svc")
102+
103+
with ot_tracer.start_as_current_span("redis_get"):
104+
self.assertIsNone(self.redis_client.get("cheese"))
105+
106+
spans = self.memory_exporter.get_finished_spans()
107+
self.assertEqual(len(spans), 2)
108+
child_span, parent_span = spans[0], spans[1]
109+
110+
# confirm the parenting
111+
self.assertIsNone(parent_span.parent)
112+
self.assertIs(child_span.parent, parent_span.get_context())
113+
114+
self.assertEqual(parent_span.name, "redis_get")
115+
self.assertEqual(parent_span.instrumentation_info.name, "redis_svc")
116+
117+
self.assertEqual(
118+
child_span.attributes.get("service"), self.test_service
119+
)
120+
self.assertEqual(child_span.name, "redis.command")
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
- Initial release
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
graft src
2+
graft tests
3+
global-exclude *.pyc
4+
global-exclude *.pyo
5+
global-exclude __pycache__/*
6+
include CHANGELOG.md
7+
include MANIFEST.in
8+
include README.rst
9+
include LICENSE
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
OpenTelemetry Redis Instrumentation
2+
===================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-redis.svg
7+
:target: https://pypi.org/project/opentelemetry-ext-redis/
8+
9+
This library allows tracing requests made by the Redis library.
10+
11+
Installation
12+
------------
13+
14+
::
15+
16+
pip install opentelemetry-ext-redis
17+
18+
19+
References
20+
----------
21+
22+
* `OpenTelemetry Redis Instrumentation <https://opentelemetry-python.readthedocs.io/en/latest/ext/opentelemetry-ext-redis/opentelemetry-ext-redis.html>`_
23+
* `OpenTelemetry Project <https://opentelemetry.io/>`_

ext/opentelemetry-ext-redis/setup.cfg

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
[metadata]
16+
name = opentelemetry-ext-redis
17+
description = Redis tracing for OpenTelemetry
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = [email protected]
22+
url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-redis
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 4 - Beta
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.4
32+
Programming Language :: Python :: 3.5
33+
Programming Language :: Python :: 3.6
34+
Programming Language :: Python :: 3.7
35+
Programming Language :: Python :: 3.8
36+
37+
[options]
38+
python_requires = >=3.4
39+
package_dir=
40+
=src
41+
packages=find_namespace:
42+
install_requires =
43+
opentelemetry-api == 0.7dev0
44+
opentelemetry-auto-instrumentation == 0.7dev0
45+
redis >= 2.6
46+
wrapt >= 1.12.1
47+
48+
[options.extras_require]
49+
test =
50+
opentelemetry-test == 0.7.dev0
51+
opentelemetry-sdk == 0.7dev0
52+
53+
[options.packages.find]
54+
where = src
55+
56+
[options.entry_points]
57+
opentelemetry_instrumentor =
58+
redis = opentelemetry.ext.redis:RedisInstrumentor

0 commit comments

Comments
 (0)