Skip to content

Instrumenting Redis #21

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

Closed
wants to merge 53 commits into from
Closed
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7262fbb
move redis directory
Apr 3, 2020
b3cb5c5
move tests
Apr 3, 2020
a11a593
create opentelemetry-ext-redis package
Apr 3, 2020
9d63b7e
fix import paths
Apr 3, 2020
a0e9498
update code to use opentelemetry API
Apr 4, 2020
2b38380
removing legacy code
Apr 4, 2020
4c2cfea
Update setup.cfg
Apr 4, 2020
38daba2
fix tests
Apr 6, 2020
514ea82
fix naming to use instrumentation
Apr 9, 2020
ca0ba2e
Merge remote-tracking branch 'origin/master' into instrumentor-redis
Apr 9, 2020
f669783
cleaning up code
Apr 9, 2020
a6c2626
Merge remote-tracking branch 'origin/master' into instrumentor-redis
Apr 9, 2020
bcd8d76
more cleanup
Apr 9, 2020
064c33a
fix readme
Apr 9, 2020
6500779
fixing tox, ignore reference for pytest
Apr 9, 2020
aa19ed4
adding docker compose file
Apr 9, 2020
fdac399
cleanup tox, add dep for py34
Apr 9, 2020
a9c866e
adding constraint for ddtrace version
Apr 9, 2020
ceea9d1
fix dep
Apr 9, 2020
7d9883d
fix isort
Apr 9, 2020
7d8fbcb
removing some dependency on ddtrace
Apr 9, 2020
aff183f
removing dep on ddtrace
Apr 10, 2020
40aa813
remove from requirements
Apr 10, 2020
dea67fe
review cleanup
Apr 10, 2020
e3d35c7
set attributes for what used to be metric
Apr 10, 2020
20ed1ac
cleaning up tests
Apr 10, 2020
50f581f
more test cleanup
Apr 10, 2020
52b6920
more cleanups
Apr 10, 2020
6166485
cleaning up a bunch of variables
Apr 10, 2020
3d8305c
updating name
Apr 10, 2020
ba30d9f
more fixes
Apr 10, 2020
f307d57
Apply suggestions from code review
Apr 14, 2020
53ffc26
cleanup
Apr 14, 2020
8be6885
updating assert statements
Apr 14, 2020
6739b32
removing hyphen
Apr 14, 2020
48307af
more feedback updates
Apr 14, 2020
4102b37
removing unnecessary dict
Apr 14, 2020
f1c1702
set versions
Apr 14, 2020
98b4475
updating docs
Apr 14, 2020
591f4d7
removing comment
Apr 14, 2020
3b63eff
revert change to use full query in span name, more feedback fixes
Apr 14, 2020
cb3030e
updating docs
Apr 14, 2020
e294fbc
sdk is not required
Apr 14, 2020
e96f43c
test still needs the sdk
Apr 14, 2020
676c349
fix missing dep
Apr 15, 2020
c53e2aa
pin otel dependencies
Apr 15, 2020
b63e7c2
move entrypoint to setup.cfg
Apr 15, 2020
f71f885
use semantic conventions, remove useless test
Apr 15, 2020
18a847f
Update instrumentation/opentelemetry-instrumentation-redis/setup.cfg
Apr 15, 2020
8e1b66e
cleaning up formatting method, updating test
Apr 16, 2020
ae4def8
don't get a tracer on each operation
Apr 16, 2020
18c247a
updating test
Apr 16, 2020
798d655
method was used in the context of Pin
Apr 16, 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 dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ sphinx~=2.1
sphinx-rtd-theme~=0.4
sphinx-autodoc-typehints~=1.10.2
pytest!=5.2.3
pytest-cov>=2.8
pytest-cov>=2.8
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: "3"
# remember to use this compose file __ONLY__ for development/testing purposes

services:
redis:
image: redis:4.0-alpine
ports:
- "127.0.0.1:6379:6379"
23 changes: 23 additions & 0 deletions instrumentation/opentelemetry-instrumentation-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-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 <https://opentelemetry-python.readthedocs.io/en/latest/instrumentation/opentelemetry-instrumentation-redis/opentelemetry-instrumentation-redis.html>`_
* `OpenTelemetry Project <https://opentelemetry.io/>`_
53 changes: 53 additions & 0 deletions instrumentation/opentelemetry-instrumentation-redis/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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-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-contrib/instrumentation/opentelemetry-instrumentation-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 =
wrapt >= 1.12.1
opentelemetry-api >= 0.6b0
opentelemetry-auto-instrumentation >= 0.6b0

[options.extras_require]
test =
opentelemetry-sdk >= 0.6b0
redis

[options.packages.find]
where = src
33 changes: 33 additions & 0 deletions instrumentation/opentelemetry-instrumentation-redis/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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 os

import setuptools

BASE_DIR = os.path.dirname(__file__)
VERSION_FILENAME = os.path.join(
BASE_DIR, "src", "opentelemetry", "instrumentation", "redis", "version.py"
)
PACKAGE_INFO = {}
with open(VERSION_FILENAME) as f:
exec(f.read(), PACKAGE_INFO)

setuptools.setup(
version=PACKAGE_INFO["__version__"],
entry_points={
"opentelemetry_instrumentor": [
"redis = opentelemetry.instrumentation.redis:RedisInstrumentor"
]
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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 redis to report Redis queries.

There are two options for instrumenting code. The first option is to use
the `opentelemetry-auto-instrumentation` executable which will automatically
patch your Redis client. The second is to programmatically enable
instrumentation via the following code:

::

from opentelemetry.instrumentation.redis.patch import patch
import redis

# You can patch redis specifically
patch()

Choose a reason for hiding this comment

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

I strongly think this should be using also the instrumentor interface, but will ignore for now until we clarify those ideas.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, I'd rather not block here until the auto-instrumentation interface is completed. Here's an issue to track this: #38


# This will report a span with the default settings
client = redis.StrictRedis(host="localhost", port=6379)
client.get("my-key")
"""
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.redis.patch import patch, unpatch


class RedisInstrumentor(BaseInstrumentor):
"""An instrumentor for Redis
See `BaseInstrumentor`
"""

def _instrument(self):
patch()

def _uninstrument(self):
unpatch()
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 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.
#
# pylint:disable=relative-beyond-top-level
import redis
from wrapt import ObjectProxy, wrap_function_wrapper

from opentelemetry import trace

from .util import _extract_conn_attributes, _format_command_args
from .version import __version__

_DEFAULT_SERVICE = "redis"
_RAWCMD = "redis.raw_command"
_CMD = "redis.command"


def patch():
"""Patch the instrumented methods

This duplicated doesn't look nice. The nicer alternative is to use an ObjectProxy on top
of Redis and StrictRedis. However, it means that any "import redis.Redis" won't be instrumented.
"""
if getattr(redis, "_opentelemetry_patch", False):
return
setattr(redis, "_opentelemetry_patch", True)

if redis.VERSION < (3, 0, 0):
wrap_function_wrapper(
"redis", "StrictRedis.execute_command", traced_execute_command
)
wrap_function_wrapper("redis", "StrictRedis.pipeline", traced_pipeline)
wrap_function_wrapper("redis", "Redis.pipeline", traced_pipeline)
wrap_function_wrapper(
"redis.client", "BasePipeline.execute", traced_execute_pipeline
)
wrap_function_wrapper(
"redis.client",
"BasePipeline.immediate_execute_command",
traced_execute_command,
)
else:
wrap_function_wrapper(
"redis", "Redis.execute_command", traced_execute_command
)
wrap_function_wrapper("redis", "Redis.pipeline", traced_pipeline)
wrap_function_wrapper(
"redis.client", "Pipeline.execute", traced_execute_pipeline
)
wrap_function_wrapper(
"redis.client",
"Pipeline.immediate_execute_command",
traced_execute_command,
)


def unwrap(obj, attr):
func = getattr(obj, attr, None)
if isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
setattr(obj, attr, func.__wrapped__)


def unpatch():
if getattr(redis, "_opentelemetry_patch", False):
setattr(redis, "_opentelemetry_patch", False)
if redis.VERSION < (3, 0, 0):
unwrap(redis.StrictRedis, "execute_command")
unwrap(redis.StrictRedis, "pipeline")
unwrap(redis.Redis, "pipeline")
unwrap(
redis.client.BasePipeline, # pylint:disable=no-member
"execute",
)
unwrap(
redis.client.BasePipeline, # pylint:disable=no-member
"immediate_execute_command",
)
else:
unwrap(redis.Redis, "execute_command")
unwrap(redis.Redis, "pipeline")
unwrap(redis.client.Pipeline, "execute")
unwrap(redis.client.Pipeline, "immediate_execute_command")


def traced_execute_command(func, instance, args, kwargs):
tracer = trace.get_tracer(_DEFAULT_SERVICE, __version__)

Choose a reason for hiding this comment

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

I think this need some more discussion, creating a new tracer for each operation doesn't look like a good solution to me. Somehow the integration should have access to a tracer and avoid creating it for each operation call. I opened an issue for a related discussion open-telemetry/opentelemetry-python#585.

query = _format_command_args(args)
with tracer.start_as_current_span(_CMD) as span:
span.set_attribute("service", tracer.instrumentation_info.name)
span.set_attribute(_RAWCMD, query)
_set_connection_attributes(span, instance)
span.set_attribute("redis.args_length", len(args))
return func(*args, **kwargs)


# pylint: disable=unused-argument
def traced_pipeline(func, instance, args, kwargs):
return func(*args, **kwargs)


def traced_execute_pipeline(func, instance, args, kwargs):
tracer = trace.get_tracer(_DEFAULT_SERVICE, __version__)

cmds = [_format_command_args(c) for c, _ in instance.command_stack]
resource = "\n".join(cmds)

with tracer.start_as_current_span(_CMD) as span:
span.set_attribute("service", tracer.instrumentation_info.name)
span.set_attribute(_RAWCMD, resource)
_set_connection_attributes(span, instance)
span.set_attribute(
"redis.pipeline_length", len(instance.command_stack)
)
return func(*args, **kwargs)


def _set_connection_attributes(span, conn):
for key, value in _extract_conn_attributes(
conn.connection_pool.connection_kwargs
).items():
span.set_attribute(key, value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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
"""


def _extract_conn_attributes(conn_kwargs):
""" Transform redis conn info into dict """
try:
return {
"out.host": conn_kwargs["host"],
"out.port": conn_kwargs["port"],
"out.redis_db": conn_kwargs["db"] or 0,
}
except KeyError:
return {}


def _format_command_args(args):
"""Format a command by removing unwanted values

Restrict what we keep from the values sent (with a SET, HGET, LPUSH, ...):
- Skip binary content
- Truncate
"""
value_placeholder = "?"
value_max_len = 100
value_too_long_mark = "..."
cmd_max_len = 1000
length = 0
out = []
for arg in args:
try:
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)
except Exception: # pylint: disable=broad-except
out.append(value_placeholder)
break

return " ".join(out)
Original file line number Diff line number Diff line change
@@ -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.7.dev0"
Empty file.
Loading