diff --git a/.github/component_owners.yml b/.github/component_owners.yml
index 25a0a06e4b..51b3ab07bc 100644
--- a/.github/component_owners.yml
+++ b/.github/component_owners.yml
@@ -16,6 +16,12 @@ components:
- lukeina2z
- yiyuan-he
+ instrumentation/opentelemetry-instrumentation-redis-valkey-base:
+ - sightseeker
+
+ instrumentation/opentelemetry-instrumentation-valkey:
+ - sightseeker
+
instrumentation/opentelemetry-instrumentation-pymssql:
- guillaumep
diff --git a/.github/workflows/core_contrib_test.yml b/.github/workflows/core_contrib_test.yml
index d9f90d57d8..79f9dcfefc 100644
--- a/.github/workflows/core_contrib_test.yml
+++ b/.github/workflows/core_contrib_test.yml
@@ -2697,6 +2697,29 @@ jobs:
- name: Run tests
run: tox -e py310-test-instrumentation-redis -- -ra
+ py310-test-instrumentation-valkey:
+ name: instrumentation-valkey
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout contrib repo @ SHA - ${{ env.CONTRIB_REPO_SHA }}
+ uses: actions/checkout@v4
+ with:
+ repository: open-telemetry/opentelemetry-python-contrib
+ ref: ${{ env.CONTRIB_REPO_SHA }}
+
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.10"
+ architecture: "x64"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e py310-test-instrumentation-valkey -- -ra
+
py310-test-instrumentation-remoulade:
name: instrumentation-remoulade
runs-on: ubuntu-latest
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index ed8b69922c..32c4fc6945 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -954,6 +954,24 @@ jobs:
- name: Run tests
run: tox -e lint-instrumentation-redis
+ lint-instrumentation-valkey:
+ name: instrumentation-valkey
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout repo @ SHA - ${{ github.sha }}
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.13
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+
+ - name: Install tox
+ run: pip install tox-uv
+
+ - name: Run tests
+ run: tox -e lint-instrumentation-valkey
lint-instrumentation-remoulade:
name: instrumentation-remoulade
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 090e996805..5e8aabd1cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
+
+- `opentelemetry-instrumentation-valkey` Instrumentation for Valkey
+ ([#3478](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3478))
+- `opentelemetry-instrumentation-redis-valkey-base` Shared base package for Redis and Valkey instrumentation,
+ extracting common tracing logic into a reusable module
+ ([#3478](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3478))
- Add `BaggageLogProcessor` to `opentelemetry-processor-baggage`
([#4371](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4371))
diff --git a/instrumentation/README.md b/instrumentation/README.md
index 8d9a247945..ff5399ecb7 100644
--- a/instrumentation/README.md
+++ b/instrumentation/README.md
@@ -50,4 +50,5 @@
| [opentelemetry-instrumentation-tortoiseorm](./opentelemetry-instrumentation-tortoiseorm) | tortoise-orm >= 0.17.0 | No | development
| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | Yes | migration
| [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 3.0.0 | Yes | migration
+| [opentelemetry-instrumentation-valkey](./opentelemetry-instrumentation-valkey) | valkey >= 6.0.0 | No | development
| [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes | migration
\ No newline at end of file
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/LICENSE b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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-redis-valkey-base/README.rst b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/README.rst
new file mode 100644
index 0000000000..e2cf6e725b
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/README.rst
@@ -0,0 +1,25 @@
+OpenTelemetry Redis/Valkey Instrumentation Base
+================================================
+
+|pypi|
+
+.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-redis-valkey-base.svg
+ :target: https://pypi.org/project/opentelemetry-instrumentation-redis-valkey-base/
+
+This package provides shared instrumentation logic used by both
+``opentelemetry-instrumentation-redis`` and ``opentelemetry-instrumentation-valkey``.
+
+It is not intended to be used directly. Install one of the backend-specific
+packages instead.
+
+Installation
+------------
+
+::
+
+ pip install opentelemetry-instrumentation-redis-valkey-base
+
+References
+----------
+
+* `OpenTelemetry Project `_
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/pyproject.toml b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/pyproject.toml
new file mode 100644
index 0000000000..6f69339aa7
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/pyproject.toml
@@ -0,0 +1,51 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "opentelemetry-instrumentation-redis-valkey-base"
+dynamic = ["version"]
+description = "OpenTelemetry shared base for Redis and Valkey instrumentation"
+readme = "README.rst"
+license = "Apache-2.0"
+requires-python = ">=3.10"
+authors = [
+ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
+]
+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.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+]
+dependencies = [
+ "opentelemetry-api ~= 1.12",
+ "opentelemetry-instrumentation == 0.63b0.dev",
+ "opentelemetry-semantic-conventions == 0.63b0.dev",
+ "wrapt >= 1.0.0, < 2.0.0",
+]
+
+[project.optional-dependencies]
+instruments = []
+
+[project.urls]
+Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-redis-valkey-base"
+Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"
+
+[tool.hatch.version]
+path = "src/opentelemetry/instrumentation/_redis_valkey/version.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/src",
+ "/tests",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/opentelemetry"]
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/__init__.py b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/__init__.py
new file mode 100644
index 0000000000..71b8341544
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/__init__.py
@@ -0,0 +1,253 @@
+# 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.
+#
+"""
+Shared instrumentation base for Redis and Valkey.
+
+This module provides parameterized factory functions and utilities used by
+both ``opentelemetry-instrumentation-redis`` and
+``opentelemetry-instrumentation-valkey``.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Callable
+
+from opentelemetry import trace
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _add_create_attributes,
+ _add_search_attributes,
+ _build_span_meta_data_for_pipeline,
+ _build_span_name,
+ _format_command_args,
+ _set_connection_attributes,
+)
+from opentelemetry.instrumentation.utils import is_instrumentation_enabled
+from opentelemetry.semconv._incubating.attributes.db_attributes import (
+ DB_STATEMENT,
+)
+from opentelemetry.trace import StatusCode, Tracer
+
+
+@dataclass(frozen=True)
+class KVStoreConfig:
+ """Configuration for a key-value store backend (Redis or Valkey).
+
+ All backend-specific differences are captured here so that the shared
+ factory functions can be used by both Redis and Valkey instrumentations.
+ """
+
+ # Identity
+ backend_name: str # "redis" or "valkey"
+ db_system: str # "redis" or "valkey"
+
+ # Span attribute keys
+ db_system_attr: str # DB_SYSTEM semconv key
+ db_index_attr: str # DB_REDIS_DATABASE_INDEX or "db.valkey.database_index"
+ args_length_attr: str # "db.redis.args_length" or "db.valkey.args_length"
+ pipeline_length_attr: (
+ str # "db.redis.pipeline_length" or "db.valkey.pipeline_length"
+ )
+
+ # WatchError class from the backend library
+ watch_error_class: type
+
+
+def _traced_execute_factory(
+ config: KVStoreConfig,
+ tracer: Tracer,
+ request_hook: Callable | None = None,
+ response_hook: Callable | None = None,
+):
+ """Create a wrapper for execute_command that creates spans."""
+
+ def _traced_execute_command(func, instance, args, kwargs):
+ if not is_instrumentation_enabled():
+ return func(*args, **kwargs)
+
+ query = _format_command_args(args)
+ name = _build_span_name(instance, args, config.backend_name)
+ with tracer.start_as_current_span(
+ name, kind=trace.SpanKind.CLIENT
+ ) as span:
+ if span.is_recording():
+ span.set_attribute(DB_STATEMENT, query)
+ _set_connection_attributes(
+ span,
+ instance,
+ config.db_system,
+ config.db_system_attr,
+ config.db_index_attr,
+ )
+ span.set_attribute(config.args_length_attr, len(args))
+ if span.name == f"{config.backend_name}.create_index":
+ _add_create_attributes(span, args, config.backend_name)
+ if callable(request_hook):
+ request_hook(span, instance, args, kwargs)
+ response = func(*args, **kwargs)
+ if span.is_recording():
+ if span.name == f"{config.backend_name}.search":
+ _add_search_attributes(
+ span, response, args, config.backend_name
+ )
+ if callable(response_hook):
+ response_hook(span, instance, response)
+ return response
+
+ return _traced_execute_command
+
+
+def _traced_execute_pipeline_factory(
+ config: KVStoreConfig,
+ tracer: Tracer,
+ request_hook: Callable | None = None,
+ response_hook: Callable | None = None,
+):
+ """Create a wrapper for pipeline execute that creates spans."""
+
+ def _traced_execute_pipeline(func, instance, args, kwargs):
+ if not is_instrumentation_enabled():
+ return func(*args, **kwargs)
+
+ (
+ command_stack,
+ resource,
+ span_name,
+ ) = _build_span_meta_data_for_pipeline(instance, config.backend_name)
+ exception = None
+ with tracer.start_as_current_span(
+ span_name, kind=trace.SpanKind.CLIENT
+ ) as span:
+ if span.is_recording():
+ span.set_attribute(DB_STATEMENT, resource)
+ _set_connection_attributes(
+ span,
+ instance,
+ config.db_system,
+ config.db_system_attr,
+ config.db_index_attr,
+ )
+ span.set_attribute(
+ config.pipeline_length_attr, len(command_stack)
+ )
+
+ response = None
+ try:
+ response = func(*args, **kwargs)
+ except config.watch_error_class as watch_exception:
+ span.set_status(StatusCode.UNSET)
+ exception = watch_exception
+
+ if callable(response_hook):
+ response_hook(span, instance, response)
+
+ if exception:
+ raise exception
+
+ return response
+
+ return _traced_execute_pipeline
+
+
+def _async_traced_execute_factory(
+ config: KVStoreConfig,
+ tracer: Tracer,
+ request_hook: Callable | None = None,
+ response_hook: Callable | None = None,
+):
+ """Create an async wrapper for execute_command that creates spans."""
+
+ async def _async_traced_execute_command(func, instance, args, kwargs):
+ if not is_instrumentation_enabled():
+ return await func(*args, **kwargs)
+
+ query = _format_command_args(args)
+ name = _build_span_name(instance, args, config.backend_name)
+
+ with tracer.start_as_current_span(
+ name, kind=trace.SpanKind.CLIENT
+ ) as span:
+ if span.is_recording():
+ span.set_attribute(DB_STATEMENT, query)
+ _set_connection_attributes(
+ span,
+ instance,
+ config.db_system,
+ config.db_system_attr,
+ config.db_index_attr,
+ )
+ span.set_attribute(config.args_length_attr, len(args))
+ if callable(request_hook):
+ request_hook(span, instance, args, kwargs)
+ response = await func(*args, **kwargs)
+ if callable(response_hook):
+ response_hook(span, instance, response)
+ return response
+
+ return _async_traced_execute_command
+
+
+def _async_traced_execute_pipeline_factory(
+ config: KVStoreConfig,
+ tracer: Tracer,
+ request_hook: Callable | None = None,
+ response_hook: Callable | None = None,
+):
+ """Create an async wrapper for pipeline execute that creates spans."""
+
+ async def _async_traced_execute_pipeline(func, instance, args, kwargs):
+ if not is_instrumentation_enabled():
+ return await func(*args, **kwargs)
+
+ (
+ command_stack,
+ resource,
+ span_name,
+ ) = _build_span_meta_data_for_pipeline(instance, config.backend_name)
+
+ exception = None
+
+ with tracer.start_as_current_span(
+ span_name, kind=trace.SpanKind.CLIENT
+ ) as span:
+ if span.is_recording():
+ span.set_attribute(DB_STATEMENT, resource)
+ _set_connection_attributes(
+ span,
+ instance,
+ config.db_system,
+ config.db_system_attr,
+ config.db_index_attr,
+ )
+ span.set_attribute(
+ config.pipeline_length_attr, len(command_stack)
+ )
+
+ response = None
+ try:
+ response = await func(*args, **kwargs)
+ except config.watch_error_class as watch_exception:
+ span.set_status(StatusCode.UNSET)
+ exception = watch_exception
+
+ if callable(response_hook):
+ response_hook(span, instance, response)
+
+ if exception:
+ raise exception
+
+ return response
+
+ return _async_traced_execute_pipeline
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/package.py b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/package.py
new file mode 100644
index 0000000000..292ae46b67
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/package.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.
+
+_instruments = ()
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/py.typed b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/util.py b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/util.py
new file mode 100644
index 0000000000..414d7d2687
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/util.py
@@ -0,0 +1,227 @@
+# 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.
+#
+"""
+Shared utility functions for Redis/Valkey instrumentation.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from opentelemetry.semconv.attributes.server_attributes import (
+ SERVER_ADDRESS,
+ SERVER_PORT,
+)
+from opentelemetry.trace import Span
+
+_FIELD_TYPES = ["NUMERIC", "TEXT", "GEO", "TAG", "VECTOR"]
+
+
+def _extract_conn_attributes(
+ conn_kwargs: dict[str, Any],
+ db_system: str,
+ db_system_attr: str,
+ db_index_attr: str,
+) -> dict[str, Any]:
+ """Transform connection info into a dict of span attributes.
+
+ Args:
+ conn_kwargs: Connection keyword arguments from the client's connection pool.
+ db_system: The database system name (e.g., "redis" or "valkey").
+ db_system_attr: The attribute key for DB_SYSTEM.
+ db_index_attr: The attribute key for the database index.
+ """
+ attributes: dict[str, Any] = {
+ db_system_attr: db_system,
+ }
+ db = conn_kwargs.get("db", 0)
+ attributes[db_index_attr] = db
+ if "path" in conn_kwargs:
+ attributes[SERVER_ADDRESS] = conn_kwargs.get("path", "")
+ else:
+ attributes[SERVER_ADDRESS] = conn_kwargs.get("host", "localhost")
+ attributes[SERVER_PORT] = conn_kwargs.get("port", 6379)
+
+ return attributes
+
+
+def _format_command_args(args: list[str]) -> str:
+ """Format and sanitize command arguments, and trim them as needed."""
+ cmd_max_len = 1000
+ value_too_long_mark = "..."
+
+ # Sanitized query format: "COMMAND ? ?"
+ args_length = len(args)
+ if args_length > 0:
+ out = [str(args[0])] + ["?"] * (args_length - 1)
+ out_str = " ".join(out)
+
+ if len(out_str) > cmd_max_len:
+ out_str = (
+ out_str[: cmd_max_len - len(value_too_long_mark)]
+ + value_too_long_mark
+ )
+ else:
+ out_str = ""
+
+ return out_str
+
+
+def _set_span_attribute_if_value(span: Span, name: str, value: Any) -> None:
+ if value is not None and value != "":
+ span.set_attribute(name, value)
+
+
+def _value_or_none(values: Any, n: int) -> Any:
+ try:
+ return values[n]
+ except IndexError:
+ return None
+
+
+def _set_connection_attributes(
+ span: Span,
+ conn: Any,
+ db_system: str,
+ db_system_attr: str,
+ db_index_attr: str,
+) -> None:
+ """Set connection attributes on a span from a client instance."""
+ if not span.is_recording() or not hasattr(conn, "connection_pool"):
+ return
+ for key, value in _extract_conn_attributes(
+ conn.connection_pool.connection_kwargs,
+ db_system,
+ db_system_attr,
+ db_index_attr,
+ ).items():
+ span.set_attribute(key, value)
+
+
+def _build_span_name(
+ instance: Any,
+ cmd_args: tuple[Any, ...],
+ backend_name: str,
+) -> str:
+ """Build a span name from the command arguments.
+
+ Args:
+ instance: The client instance.
+ cmd_args: The command arguments.
+ backend_name: The backend name (e.g., "redis" or "valkey").
+ """
+ if len(cmd_args) > 0 and cmd_args[0]:
+ if cmd_args[0] == "FT.SEARCH":
+ name = f"{backend_name}.search"
+ elif cmd_args[0] == "FT.CREATE":
+ name = f"{backend_name}.create_index"
+ else:
+ name = cmd_args[0]
+ else:
+ name = instance.connection_pool.connection_kwargs.get("db", 0)
+ return name
+
+
+def _add_create_attributes(
+ span: Span,
+ args: tuple[Any, ...],
+ backend_name: str,
+) -> None:
+ """Add FT.CREATE index attributes to a span."""
+ _set_span_attribute_if_value(
+ span, f"{backend_name}.create_index.index", _value_or_none(args, 1)
+ )
+ try:
+ schema_index = args.index("SCHEMA")
+ except ValueError:
+ return
+ schema = args[schema_index:]
+ field_attribute = "".join(
+ f"Field(name: {schema[index - 1]}, type: {schema[index]});"
+ for index in range(1, len(schema))
+ if schema[index] in _FIELD_TYPES
+ )
+ _set_span_attribute_if_value(
+ span,
+ f"{backend_name}.create_index.fields",
+ field_attribute,
+ )
+
+
+def _add_search_attributes(
+ span: Span,
+ response: Any,
+ args: tuple[Any, ...],
+ backend_name: str,
+) -> None:
+ """Add FT.SEARCH response attributes to a span."""
+ _set_span_attribute_if_value(
+ span, f"{backend_name}.search.index", _value_or_none(args, 1)
+ )
+ _set_span_attribute_if_value(
+ span, f"{backend_name}.search.query", _value_or_none(args, 2)
+ )
+ number_of_returned_documents = _value_or_none(response, 0)
+ _set_span_attribute_if_value(
+ span, f"{backend_name}.search.total", number_of_returned_documents
+ )
+ if "NOCONTENT" in args or not number_of_returned_documents:
+ return
+ for document_number in range(number_of_returned_documents):
+ document_index = _value_or_none(response, 1 + 2 * document_number)
+ if document_index:
+ document = response[2 + 2 * document_number]
+ for attribute_name_index in range(0, len(document), 2):
+ _set_span_attribute_if_value(
+ span,
+ f"{backend_name}.search.xdoc_{document_index}.{document[attribute_name_index]}",
+ document[attribute_name_index + 1],
+ )
+
+
+def _build_span_meta_data_for_pipeline(
+ instance: Any,
+ backend_name: str,
+) -> tuple[list[Any], str, str]:
+ """Build span metadata for a pipeline execution.
+
+ Returns:
+ A tuple of (command_stack, resource, span_name).
+ """
+ try:
+ command_stack = (
+ instance.command_stack
+ if hasattr(instance, "command_stack")
+ else instance._command_stack
+ )
+
+ cmds = [
+ _format_command_args(c.args if hasattr(c, "args") else c[0])
+ for c in command_stack
+ ]
+ resource = "\n".join(cmds)
+
+ span_name = " ".join(
+ [
+ (c.args[0] if hasattr(c, "args") else c[0][0])
+ for c in command_stack
+ ]
+ )
+ except (AttributeError, IndexError):
+ command_stack = []
+ resource = ""
+ span_name = ""
+
+ return command_stack, resource, span_name or backend_name
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/version.py b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/version.py
new file mode 100644
index 0000000000..a07bc2663e
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/src/opentelemetry/instrumentation/_redis_valkey/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.63b0.dev"
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/test-requirements.txt
new file mode 100644
index 0000000000..39ba46f0d3
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/test-requirements.txt
@@ -0,0 +1,9 @@
+iniconfig==2.0.0
+packaging==24.0
+pluggy==1.5.0
+pytest==7.4.4
+tomli==2.0.1
+typing_extensions==4.12.2
+wrapt==1.16.0
+-e opentelemetry-instrumentation
+-e instrumentation/opentelemetry-instrumentation-redis-valkey-base
diff --git a/instrumentation/opentelemetry-instrumentation-redis-valkey-base/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-redis-valkey-base/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml b/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml
index 19c5e194f9..34e1bf3fc0 100644
--- a/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml
+++ b/instrumentation/opentelemetry-instrumentation-redis/pyproject.toml
@@ -27,6 +27,7 @@ classifiers = [
dependencies = [
"opentelemetry-api ~= 1.12",
"opentelemetry-instrumentation == 0.63b0.dev",
+ "opentelemetry-instrumentation-redis-valkey-base == 0.63b0.dev",
"opentelemetry-semantic-conventions == 0.63b0.dev",
"wrapt >= 1.12.1",
]
diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py
index 194351c7da..e181fe1d7b 100644
--- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py
@@ -135,52 +135,31 @@ def response_hook(span, instance, response):
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING, Any, Callable, Collection
+from typing import TYPE_CHECKING, Any, Collection
import redis
from wrapt import wrap_function_wrapper
-from opentelemetry import trace
+from opentelemetry.instrumentation._redis_valkey import (
+ KVStoreConfig,
+ _async_traced_execute_factory,
+ _async_traced_execute_pipeline_factory,
+ _traced_execute_factory,
+ _traced_execute_pipeline_factory,
+)
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.redis.package import _instruments
-from opentelemetry.instrumentation.redis.util import (
- _add_create_attributes,
- _add_search_attributes,
- _build_span_meta_data_for_pipeline,
- _build_span_name,
- _format_command_args,
- _set_connection_attributes,
-)
from opentelemetry.instrumentation.redis.version import __version__
-from opentelemetry.instrumentation.utils import (
- is_instrumentation_enabled,
- unwrap,
-)
+from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.semconv._incubating.attributes.db_attributes import (
- DB_STATEMENT,
-)
-from opentelemetry.trace import (
- StatusCode,
- Tracer,
- TracerProvider,
- get_tracer,
+ DB_REDIS_DATABASE_INDEX,
+ DB_SYSTEM,
)
+from opentelemetry.semconv.trace import DbSystemValues
+from opentelemetry.trace import TracerProvider, get_tracer
if TYPE_CHECKING:
- from typing import Awaitable
-
- import redis.asyncio.client
- import redis.asyncio.cluster
- import redis.client
- import redis.cluster
- import redis.connection
-
from opentelemetry.instrumentation.redis.custom_types import (
- AsyncPipelineInstance,
- AsyncRedisInstance,
- PipelineInstance,
- R,
- RedisInstance,
RequestHook,
ResponseHook,
)
@@ -192,7 +171,6 @@ def response_hook(span, instance, response):
_REDIS_CLUSTER_VERSION = (4, 1, 0)
_REDIS_ASYNCIO_CLUSTER_VERSION = (4, 3, 2)
-
_CLIENT_ASYNCIO_SUPPORT = redis.VERSION >= _REDIS_ASYNCIO_VERSION
_CLIENT_ASYNCIO_CLUSTER_SUPPORT = (
redis.VERSION >= _REDIS_ASYNCIO_CLUSTER_VERSION
@@ -205,188 +183,28 @@ def response_hook(span, instance, response):
_INSTRUMENTATION_ATTR = "_is_instrumented_by_opentelemetry"
-
-def _traced_execute_factory(
- tracer: Tracer,
- request_hook: RequestHook | None = None,
- response_hook: ResponseHook | None = None,
-):
- def _traced_execute_command(
- func: Callable[..., R],
- instance: RedisInstance,
- args: tuple[Any, ...],
- kwargs: dict[str, Any],
- ) -> R:
- if not is_instrumentation_enabled():
- return func(*args, **kwargs)
-
- query = _format_command_args(args)
- name = _build_span_name(instance, args)
- with tracer.start_as_current_span(
- name, kind=trace.SpanKind.CLIENT
- ) as span:
- if span.is_recording():
- span.set_attribute(DB_STATEMENT, query)
- _set_connection_attributes(span, instance)
- span.set_attribute("db.redis.args_length", len(args))
- if span.name == "redis.create_index":
- _add_create_attributes(span, args)
- if callable(request_hook):
- request_hook(span, instance, args, kwargs)
- response = func(*args, **kwargs)
- if span.is_recording():
- if span.name == "redis.search":
- _add_search_attributes(span, response, args)
- if callable(response_hook):
- response_hook(span, instance, response)
- return response
-
- return _traced_execute_command
-
-
-def _traced_execute_pipeline_factory(
- tracer: Tracer,
- request_hook: RequestHook | None = None,
- response_hook: ResponseHook | None = None,
-):
- def _traced_execute_pipeline(
- func: Callable[..., R],
- instance: PipelineInstance,
- args: tuple[Any, ...],
- kwargs: dict[str, Any],
- ) -> R:
- if not is_instrumentation_enabled():
- return func(*args, **kwargs)
-
- (
- command_stack,
- resource,
- span_name,
- ) = _build_span_meta_data_for_pipeline(instance)
- exception = None
- with tracer.start_as_current_span(
- span_name, kind=trace.SpanKind.CLIENT
- ) as span:
- if span.is_recording():
- span.set_attribute(DB_STATEMENT, resource)
- _set_connection_attributes(span, instance)
- span.set_attribute(
- "db.redis.pipeline_length", len(command_stack)
- )
-
- response = None
- try:
- response = func(*args, **kwargs)
- except redis.WatchError as watch_exception:
- span.set_status(StatusCode.UNSET)
- exception = watch_exception
-
- if callable(response_hook):
- response_hook(span, instance, response)
-
- if exception:
- raise exception
-
- return response
-
- return _traced_execute_pipeline
-
-
-def _async_traced_execute_factory(
- tracer: Tracer,
- request_hook: RequestHook | None = None,
- response_hook: ResponseHook | None = None,
-):
- async def _async_traced_execute_command(
- func: Callable[..., Awaitable[R]],
- instance: AsyncRedisInstance,
- args: tuple[Any, ...],
- kwargs: dict[str, Any],
- ) -> Awaitable[R]:
- if not is_instrumentation_enabled():
- return await func(*args, **kwargs)
-
- query = _format_command_args(args)
- name = _build_span_name(instance, args)
-
- with tracer.start_as_current_span(
- name, kind=trace.SpanKind.CLIENT
- ) as span:
- if span.is_recording():
- span.set_attribute(DB_STATEMENT, query)
- _set_connection_attributes(span, instance)
- span.set_attribute("db.redis.args_length", len(args))
- if callable(request_hook):
- request_hook(span, instance, args, kwargs)
- response = await func(*args, **kwargs)
- if callable(response_hook):
- response_hook(span, instance, response)
- return response
-
- return _async_traced_execute_command
-
-
-def _async_traced_execute_pipeline_factory(
- tracer: Tracer,
- request_hook: RequestHook | None = None,
- response_hook: ResponseHook | None = None,
-):
- async def _async_traced_execute_pipeline(
- func: Callable[..., Awaitable[R]],
- instance: AsyncPipelineInstance,
- args: tuple[Any, ...],
- kwargs: dict[str, Any],
- ) -> Awaitable[R]:
- if not is_instrumentation_enabled():
- return await func(*args, **kwargs)
-
- (
- command_stack,
- resource,
- span_name,
- ) = _build_span_meta_data_for_pipeline(instance)
-
- exception = None
-
- with tracer.start_as_current_span(
- span_name, kind=trace.SpanKind.CLIENT
- ) as span:
- if span.is_recording():
- span.set_attribute(DB_STATEMENT, resource)
- _set_connection_attributes(span, instance)
- span.set_attribute(
- "db.redis.pipeline_length", len(command_stack)
- )
-
- response = None
- try:
- response = await func(*args, **kwargs)
- except redis.WatchError as watch_exception:
- span.set_status(StatusCode.UNSET)
- exception = watch_exception
-
- if callable(response_hook):
- response_hook(span, instance, response)
-
- if exception:
- raise exception
-
- return response
-
- return _async_traced_execute_pipeline
+_REDIS_CONFIG = KVStoreConfig(
+ backend_name="redis",
+ db_system=DbSystemValues.REDIS.value,
+ db_system_attr=DB_SYSTEM,
+ db_index_attr=DB_REDIS_DATABASE_INDEX,
+ args_length_attr="db.redis.args_length",
+ pipeline_length_attr="db.redis.pipeline_length",
+ watch_error_class=redis.WatchError,
+)
# pylint: disable=R0915
def _instrument(
- tracer: Tracer,
- request_hook: RequestHook | None = None,
- response_hook: ResponseHook | None = None,
+ tracer,
+ request_hook=None,
+ response_hook=None,
):
_traced_execute_command = _traced_execute_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
_traced_execute_pipeline = _traced_execute_pipeline_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
pipeline_class = "BasePipeline" if _CLIENT_BEFORE_V3 else "Pipeline"
redis_class = "StrictRedis" if _CLIENT_BEFORE_V3 else "Redis"
@@ -417,10 +235,10 @@ def _instrument(
)
_async_traced_execute_command = _async_traced_execute_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
_async_traced_execute_pipeline = _async_traced_execute_pipeline_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
if _CLIENT_ASYNCIO_SUPPORT:
wrap_function_wrapper(
@@ -453,16 +271,16 @@ def _instrument(
def _instrument_client(
client,
- tracer: Tracer,
- request_hook: RequestHook | None = None,
- response_hook: ResponseHook | None = None,
+ tracer,
+ request_hook=None,
+ response_hook=None,
):
# first, handle async clients and cluster clients
_async_traced_execute = _async_traced_execute_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
_async_traced_execute_pipeline = _async_traced_execute_pipeline_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
if _CLIENT_ASYNCIO_SUPPORT and isinstance(client, redis.asyncio.Redis):
@@ -500,10 +318,10 @@ def _async_cluster_pipeline_wrapper(func, instance, args, kwargs):
# for redis.client.Redis, redis.Cluster and v3.0.0 redis.client.StrictRedis
# the wrappers are the same
_traced_execute = _traced_execute_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
_traced_execute_pipeline = _traced_execute_pipeline_factory(
- tracer, request_hook, response_hook
+ _REDIS_CONFIG, tracer, request_hook, response_hook
)
def _pipeline_wrapper(func, instance, args, kwargs):
diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py
index b7d9113334..5ee3e4e40a 100644
--- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py
+++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py
@@ -2,198 +2,94 @@
# SPDX-License-Identifier: Apache-2.0
#
"""
-Some utils used by the redis integration
+Utility functions for Redis instrumentation.
+
+These functions delegate to the shared base in
+``opentelemetry.instrumentation._redis_valkey.util``, using Redis-specific
+attribute names and conventions.
"""
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import Any
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _add_create_attributes as _base_add_create_attributes,
+)
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _add_search_attributes as _base_add_search_attributes,
+)
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _build_span_meta_data_for_pipeline as _base_build_span_meta_data_for_pipeline,
+)
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _build_span_name as _base_build_span_name,
+)
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _extract_conn_attributes as _base_extract_conn_attributes,
+)
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _format_command_args,
+ _set_span_attribute_if_value,
+ _value_or_none,
+)
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _set_connection_attributes as _base_set_connection_attributes,
+)
from opentelemetry.semconv._incubating.attributes.db_attributes import (
DB_REDIS_DATABASE_INDEX,
DB_SYSTEM,
)
-from opentelemetry.semconv._incubating.attributes.net_attributes import (
- NET_PEER_NAME,
- NET_PEER_PORT,
- NET_TRANSPORT,
-)
-from opentelemetry.semconv.trace import (
- DbSystemValues,
- NetTransportValues,
-)
+from opentelemetry.semconv.trace import DbSystemValues
from opentelemetry.trace import Span
-if TYPE_CHECKING:
- from opentelemetry.instrumentation.redis.custom_types import (
- AsyncPipelineInstance,
- AsyncRedisInstance,
- PipelineInstance,
- RedisInstance,
+_BACKEND_NAME = "redis"
+_DB_SYSTEM = DbSystemValues.REDIS.value
+_DB_SYSTEM_ATTR = DB_SYSTEM
+_DB_INDEX_ATTR = DB_REDIS_DATABASE_INDEX
+
+# Re-export unchanged functions
+__all__ = [
+ "_extract_conn_attributes",
+ "_format_command_args",
+ "_set_span_attribute_if_value",
+ "_value_or_none",
+ "_set_connection_attributes",
+ "_build_span_name",
+ "_add_create_attributes",
+ "_add_search_attributes",
+ "_build_span_meta_data_for_pipeline",
+]
+
+
+def _extract_conn_attributes(conn_kwargs: dict[str, Any]) -> dict[str, Any]:
+ """Transform redis conn info into dict."""
+ return _base_extract_conn_attributes(
+ conn_kwargs, _DB_SYSTEM, _DB_SYSTEM_ATTR, _DB_INDEX_ATTR
)
-_FIELD_TYPES = ["NUMERIC", "TEXT", "GEO", "TAG", "VECTOR"]
-
-
-def _extract_conn_attributes(conn_kwargs):
- """Transform redis conn info into dict"""
- attributes = {
- DB_SYSTEM: DbSystemValues.REDIS.value,
- }
- db = conn_kwargs.get("db", 0)
- attributes[DB_REDIS_DATABASE_INDEX] = db
- if "path" in conn_kwargs:
- attributes[NET_PEER_NAME] = conn_kwargs.get("path", "")
- attributes[NET_TRANSPORT] = NetTransportValues.OTHER.value
- else:
- attributes[NET_PEER_NAME] = conn_kwargs.get("host", "localhost")
- attributes[NET_PEER_PORT] = conn_kwargs.get("port", 6379)
- attributes[NET_TRANSPORT] = NetTransportValues.IP_TCP.value
-
- return attributes
-
-def _format_command_args(args: list[str]):
- """Format and sanitize command arguments, and trim them as needed"""
- cmd_max_len = 1000
- value_too_long_mark = "..."
-
- # Sanitized query format: "COMMAND ? ?"
- args_length = len(args)
- if args_length > 0:
- out = [str(args[0])] + ["?"] * (args_length - 1)
- out_str = " ".join(out)
-
- if len(out_str) > cmd_max_len:
- out_str = (
- out_str[: cmd_max_len - len(value_too_long_mark)]
- + value_too_long_mark
- )
- else:
- out_str = ""
-
- return out_str
+def _set_connection_attributes(span: Span, conn: Any) -> None:
+ _base_set_connection_attributes(
+ span, conn, _DB_SYSTEM, _DB_SYSTEM_ATTR, _DB_INDEX_ATTR
+ )
-def _set_span_attribute_if_value(span, name, value):
- if value is not None and value != "":
- span.set_attribute(name, value)
+def _build_span_name(instance: Any, cmd_args: tuple[Any, ...]) -> str:
+ return _base_build_span_name(instance, cmd_args, _BACKEND_NAME)
-def _value_or_none(values, n):
- try:
- return values[n]
- except IndexError:
- return None
+def _add_create_attributes(span: Span, args: tuple[Any, ...]) -> None:
+ _base_add_create_attributes(span, args, _BACKEND_NAME)
-def _set_connection_attributes(
- span: Span, conn: RedisInstance | AsyncRedisInstance
+def _add_search_attributes(
+ span: Span, response: Any, args: tuple[Any, ...]
) -> None:
- if not span.is_recording() or not hasattr(conn, "connection_pool"):
- return
- for key, value in _extract_conn_attributes(
- conn.connection_pool.connection_kwargs
- ).items():
- span.set_attribute(key, value)
-
-
-def _build_span_name(
- instance: RedisInstance | AsyncRedisInstance, cmd_args: tuple[Any, ...]
-) -> str:
- if len(cmd_args) > 0 and cmd_args[0]:
- if cmd_args[0] == "FT.SEARCH":
- name = "redis.search"
- elif cmd_args[0] == "FT.CREATE":
- name = "redis.create_index"
- else:
- name = cmd_args[0]
- else:
- name = instance.connection_pool.connection_kwargs.get("db", 0)
- return name
-
-
-def _add_create_attributes(span: Span, args: tuple[Any, ...]):
- _set_span_attribute_if_value(
- span, "redis.create_index.index", _value_or_none(args, 1)
- )
- # According to: https://github.com/redis/redis-py/blob/master/redis/commands/search/commands.py#L155 schema is last argument for execute command
- try:
- schema_index = args.index("SCHEMA")
- except ValueError:
- return
- schema = args[schema_index:]
- field_attribute = ""
- # Schema in format:
- # [first_field_name, first_field_type, first_field_some_attribute1, first_field_some_attribute2, second_field_name, ...]
- field_attribute = "".join(
- f"Field(name: {schema[index - 1]}, type: {schema[index]});"
- for index in range(1, len(schema))
- if schema[index] in _FIELD_TYPES
- )
- _set_span_attribute_if_value(
- span,
- "redis.create_index.fields",
- field_attribute,
- )
-
-
-def _add_search_attributes(span: Span, response, args):
- _set_span_attribute_if_value(
- span, "redis.search.index", _value_or_none(args, 1)
- )
- _set_span_attribute_if_value(
- span, "redis.search.query", _value_or_none(args, 2)
- )
- # Parse response from search
- # https://redis.io/docs/latest/commands/ft.search/
- # Response in format:
- # [number_of_returned_documents, index_of_first_returned_doc, first_doc(as a list), index_of_second_returned_doc, second_doc(as a list) ...]
- # Returned documents in array format:
- # [first_field_name, first_field_value, second_field_name, second_field_value ...]
- number_of_returned_documents = _value_or_none(response, 0)
- _set_span_attribute_if_value(
- span, "redis.search.total", number_of_returned_documents
- )
- if "NOCONTENT" in args or not number_of_returned_documents:
- return
- for document_number in range(number_of_returned_documents):
- document_index = _value_or_none(response, 1 + 2 * document_number)
- if document_index:
- document = response[2 + 2 * document_number]
- for attribute_name_index in range(0, len(document), 2):
- _set_span_attribute_if_value(
- span,
- f"redis.search.xdoc_{document_index}.{document[attribute_name_index]}",
- document[attribute_name_index + 1],
- )
+ _base_add_search_attributes(span, response, args, _BACKEND_NAME)
def _build_span_meta_data_for_pipeline(
- instance: PipelineInstance | AsyncPipelineInstance,
+ instance: Any,
) -> tuple[list[Any], str, str]:
- try:
- command_stack = (
- instance.command_stack
- if hasattr(instance, "command_stack")
- else instance._command_stack
- )
-
- cmds = [
- _format_command_args(c.args if hasattr(c, "args") else c[0])
- for c in command_stack
- ]
- resource = "\n".join(cmds)
-
- span_name = " ".join(
- [
- (c.args[0] if hasattr(c, "args") else c[0][0])
- for c in command_stack
- ]
- )
- except (AttributeError, IndexError):
- command_stack = []
- resource = ""
- span_name = ""
-
- return command_stack, resource, span_name or "redis"
+ return _base_build_span_meta_data_for_pipeline(instance, _BACKEND_NAME)
diff --git a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt
index 627b13573c..502cec12ac 100644
--- a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt
+++ b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt
@@ -13,4 +13,5 @@ typing_extensions==4.12.2
wrapt==1.16.0
zipp==3.19.2
-e opentelemetry-instrumentation
+-e instrumentation/opentelemetry-instrumentation-redis-valkey-base
-e instrumentation/opentelemetry-instrumentation-redis
diff --git a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py
index a09e2de117..0c7ffb3285 100644
--- a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py
+++ b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py
@@ -20,11 +20,9 @@
DB_SYSTEM,
DbSystemValues,
)
-from opentelemetry.semconv._incubating.attributes.net_attributes import (
- NET_PEER_NAME,
- NET_PEER_PORT,
- NET_TRANSPORT,
- NetTransportValues,
+from opentelemetry.semconv.attributes.server_attributes import (
+ SERVER_ADDRESS,
+ SERVER_PORT,
)
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace import SpanKind
@@ -258,12 +256,8 @@ def test_attributes_default(self):
DbSystemValues.REDIS.value,
)
self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 0)
- self.assertEqual(span.attributes[NET_PEER_NAME], "localhost")
- self.assertEqual(span.attributes[NET_PEER_PORT], 6379)
- self.assertEqual(
- span.attributes[NET_TRANSPORT],
- NetTransportValues.IP_TCP.value,
- )
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 6379)
def test_attributes_tcp(self):
redis_client = redis.Redis.from_url("redis://foo:bar@1.1.1.1:6380/1")
@@ -280,12 +274,8 @@ def test_attributes_tcp(self):
DbSystemValues.REDIS.value,
)
self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 1)
- self.assertEqual(span.attributes[NET_PEER_NAME], "1.1.1.1")
- self.assertEqual(span.attributes[NET_PEER_PORT], 6380)
- self.assertEqual(
- span.attributes[NET_TRANSPORT],
- NetTransportValues.IP_TCP.value,
- )
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "1.1.1.1")
+ self.assertEqual(span.attributes[SERVER_PORT], 6380)
def test_attributes_unix_socket(self):
redis_client = redis.Redis.from_url(
@@ -305,13 +295,9 @@ def test_attributes_unix_socket(self):
)
self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 3)
self.assertEqual(
- span.attributes[NET_PEER_NAME],
+ span.attributes[SERVER_ADDRESS],
"/path/to/socket.sock",
)
- self.assertEqual(
- span.attributes[NET_TRANSPORT],
- NetTransportValues.OTHER.value,
- )
def test_connection_error(self):
server = fakeredis.FakeServer()
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/LICENSE b/instrumentation/opentelemetry-instrumentation-valkey/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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-valkey/README.rst b/instrumentation/opentelemetry-instrumentation-valkey/README.rst
new file mode 100644
index 0000000000..20a02babc2
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/README.rst
@@ -0,0 +1,23 @@
+OpenTelemetry Valkey Instrumentation
+====================================
+
+|pypi|
+
+.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-valkey.svg
+ :target: https://pypi.org/project/opentelemetry-instrumentation-valkey/
+
+This library allows tracing requests made by the Valkey library.
+
+Installation
+------------
+
+::
+
+ pip install opentelemetry-instrumentation-valkey
+
+References
+----------
+
+* `OpenTelemetry Valkey Instrumentation `_
+* `OpenTelemetry Project `_
+* `OpenTelemetry Python Examples `_
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/pyproject.toml b/instrumentation/opentelemetry-instrumentation-valkey/pyproject.toml
new file mode 100644
index 0000000000..b08c8d507c
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/pyproject.toml
@@ -0,0 +1,57 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "opentelemetry-instrumentation-valkey"
+dynamic = ["version"]
+description = "OpenTelemetry Valkey instrumentation"
+readme = "README.rst"
+license = "Apache-2.0"
+requires-python = ">=3.10"
+authors = [
+ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
+]
+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.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+]
+dependencies = [
+ "opentelemetry-api ~= 1.12",
+ "opentelemetry-instrumentation == 0.63b0.dev",
+ "opentelemetry-instrumentation-redis-valkey-base == 0.63b0.dev",
+ "opentelemetry-semantic-conventions == 0.63b0.dev",
+ "wrapt >= 1.12.1",
+]
+
+[project.optional-dependencies]
+instruments = [
+ "valkey[libvalkey] >= 6.1.0",
+]
+
+[project.entry-points.opentelemetry_instrumentor]
+valkey = "opentelemetry.instrumentation.valkey:ValkeyInstrumentor"
+
+[project.urls]
+Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-valkey"
+Repository = "https://github.com/open-telemetry/opentelemetry-python-contrib"
+
+[tool.hatch.version]
+path = "src/opentelemetry/instrumentation/valkey/version.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/src",
+ "/tests",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/opentelemetry"]
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/__init__.py b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/__init__.py
new file mode 100644
index 0000000000..70fb85e4a8
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/__init__.py
@@ -0,0 +1,474 @@
+# 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 `valkey`_ to report Valkey queries.
+
+.. _valkey: https://pypi.org/project/valkey/
+
+
+Instrument All Clients
+----------------------
+
+The easiest way to instrument all valkey client instances is by
+``ValkeyInstrumentor().instrument()``:
+
+.. code:: python
+
+ from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
+ import valkey
+
+
+ # Instrument valkey
+ ValkeyInstrumentor().instrument()
+
+ # This will report a span with the default settings
+ client = valkey.StrictValkey(host="localhost", port=6379)
+ client.get("my-key")
+
+Async Valkey clients (i.e. ``valkey.asyncio.Valkey``) are also instrumented in the same way:
+
+.. code:: python
+
+ from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
+ import valkey.asyncio
+
+
+ # Instrument valkey
+ ValkeyInstrumentor().instrument()
+
+ # This will report a span with the default settings
+ async def valkey_get():
+ client = valkey.asyncio.Valkey(host="localhost", port=6379)
+ await client.get("my-key")
+
+.. note::
+ Calling the ``instrument`` method will instrument the client classes, so any client
+ created after the ``instrument`` call will be instrumented. To instrument only a
+ single client, use :func:`ValkeyInstrumentor.instrument_client` method.
+
+Instrument Single Client
+------------------------
+
+The :func:`ValkeyInstrumentor.instrument_client` can instrument a connection instance. This is useful when there are multiple clients with a different valkey database index.
+Or, you might have a different connection pool used for an application function you
+don't want instrumented.
+
+.. code:: python
+
+ from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
+ import valkey
+
+ instrumented_client = valkey.Valkey()
+ not_instrumented_client = valkey.Valkey()
+
+ # Instrument valkey
+ ValkeyInstrumentor.instrument_client(client=instrumented_client)
+
+ # This will report a span with the default settings
+ instrumented_client.get("my-key")
+
+ # This will not have a span
+ not_instrumented_client.get("my-key")
+
+.. warning::
+ All client instances created after calling ``ValkeyInstrumentor().instrument`` will
+ be instrumented. To avoid instrumenting all clients, use
+ :func:`ValkeyInstrumentor.instrument_client` .
+
+Request/Response Hooks
+----------------------
+
+.. code:: python
+
+ from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
+ import valkey
+
+ def request_hook(span, instance, args, kwargs):
+ if span and span.is_recording():
+ span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
+
+ def response_hook(span, instance, response):
+ if span and span.is_recording():
+ span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
+
+ # Instrument valkey with hooks
+ ValkeyInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
+
+ # This will report a span with the default settings and the custom attributes added from the hooks
+ client = valkey.StrictValkey(host="localhost", port=6379)
+ client.get("my-key")
+
+Suppress Instrumentation
+------------------------
+
+You can use the ``suppress_instrumentation`` context manager to prevent instrumentation
+from being applied to specific Valkey operations. This is useful when you want to avoid
+creating spans for internal operations, health checks, or during specific code paths.
+
+.. code:: python
+
+ from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
+ from opentelemetry.instrumentation.utils import suppress_instrumentation
+ import valkey
+
+ # Instrument valkey
+ ValkeyInstrumentor().instrument()
+
+ client = valkey.StrictValkey(host="localhost", port=6379)
+
+ # This will report a span
+ client.get("my-key")
+
+ # This will NOT report a span
+ with suppress_instrumentation():
+ client.get("internal-key")
+ client.set("cache-key", "value")
+
+ # This will report a span again
+ client.get("another-key")
+
+API
+---
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING, Any, Callable, Collection
+
+import valkey
+import valkey.asyncio
+from wrapt import wrap_function_wrapper
+
+from opentelemetry.instrumentation._redis_valkey import (
+ KVStoreConfig,
+ _async_traced_execute_factory,
+ _async_traced_execute_pipeline_factory,
+ _traced_execute_factory,
+ _traced_execute_pipeline_factory,
+)
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.utils import unwrap
+from opentelemetry.instrumentation.valkey.package import _instruments
+from opentelemetry.instrumentation.valkey.version import __version__
+from opentelemetry.semconv._incubating.attributes.db_attributes import (
+ DB_REDIS_DATABASE_INDEX,
+ DB_SYSTEM,
+)
+from opentelemetry.trace import TracerProvider, get_tracer
+
+if TYPE_CHECKING:
+ import valkey.asyncio.client
+ import valkey.asyncio.cluster
+ import valkey.client
+ import valkey.cluster
+ import valkey.connection
+
+
+_logger = logging.getLogger(__name__)
+
+_INSTRUMENTATION_ATTR = "_is_instrumented_by_opentelemetry"
+
+_VALKEY_CONFIG = KVStoreConfig(
+ backend_name="valkey",
+ db_system="valkey",
+ db_system_attr=DB_SYSTEM,
+ db_index_attr=DB_REDIS_DATABASE_INDEX,
+ args_length_attr="db.redis.args_length",
+ pipeline_length_attr="db.redis.pipeline_length",
+ watch_error_class=valkey.WatchError,
+)
+
+
+# pylint: disable=R0915
+def _instrument(
+ tracer,
+ request_hook=None,
+ response_hook=None,
+):
+ _traced_execute_command = _traced_execute_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+ _traced_execute_pipeline = _traced_execute_pipeline_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+
+ wrap_function_wrapper(
+ "valkey", "Valkey.execute_command", _traced_execute_command
+ )
+ wrap_function_wrapper(
+ "valkey.client",
+ "Pipeline.execute",
+ _traced_execute_pipeline,
+ )
+ wrap_function_wrapper(
+ "valkey.client",
+ "Pipeline.immediate_execute_command",
+ _traced_execute_command,
+ )
+ wrap_function_wrapper(
+ "valkey.cluster",
+ "ValkeyCluster.execute_command",
+ _traced_execute_command,
+ )
+ wrap_function_wrapper(
+ "valkey.cluster",
+ "ClusterPipeline.execute",
+ _traced_execute_pipeline,
+ )
+
+ _async_traced_execute_command = _async_traced_execute_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+ _async_traced_execute_pipeline = _async_traced_execute_pipeline_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+
+ wrap_function_wrapper(
+ "valkey.asyncio",
+ "Valkey.execute_command",
+ _async_traced_execute_command,
+ )
+ wrap_function_wrapper(
+ "valkey.asyncio.client",
+ "Pipeline.execute",
+ _async_traced_execute_pipeline,
+ )
+ wrap_function_wrapper(
+ "valkey.asyncio.client",
+ "Pipeline.immediate_execute_command",
+ _async_traced_execute_command,
+ )
+ wrap_function_wrapper(
+ "valkey.asyncio.cluster",
+ "ValkeyCluster.execute_command",
+ _async_traced_execute_command,
+ )
+ wrap_function_wrapper(
+ "valkey.asyncio.cluster",
+ "ClusterPipeline.execute",
+ _async_traced_execute_pipeline,
+ )
+
+
+def _instrument_client(
+ client,
+ tracer,
+ request_hook=None,
+ response_hook=None,
+):
+ _async_traced_execute = _async_traced_execute_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+ _async_traced_execute_pipeline = _async_traced_execute_pipeline_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+
+ if isinstance(client, valkey.asyncio.Valkey):
+
+ def _async_pipeline_wrapper(func, instance, args, kwargs):
+ result = func(*args, **kwargs)
+ wrap_function_wrapper(
+ result, "execute", _async_traced_execute_pipeline
+ )
+ wrap_function_wrapper(
+ result, "immediate_execute_command", _async_traced_execute
+ )
+ return result
+
+ wrap_function_wrapper(client, "execute_command", _async_traced_execute)
+ wrap_function_wrapper(client, "pipeline", _async_pipeline_wrapper)
+ return
+
+ if isinstance(client, valkey.asyncio.ValkeyCluster):
+
+ def _async_cluster_pipeline_wrapper(func, instance, args, kwargs):
+ result = func(*args, **kwargs)
+ wrap_function_wrapper(
+ result, "execute", _async_traced_execute_pipeline
+ )
+ return result
+
+ wrap_function_wrapper(client, "execute_command", _async_traced_execute)
+ wrap_function_wrapper(
+ client, "pipeline", _async_cluster_pipeline_wrapper
+ )
+ return
+
+ _traced_execute = _traced_execute_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+ _traced_execute_pipeline = _traced_execute_pipeline_factory(
+ _VALKEY_CONFIG, tracer, request_hook, response_hook
+ )
+
+ def _pipeline_wrapper(func, instance, args, kwargs):
+ result = func(*args, **kwargs)
+ wrap_function_wrapper(result, "execute", _traced_execute_pipeline)
+ wrap_function_wrapper(
+ result, "immediate_execute_command", _traced_execute
+ )
+ return result
+
+ wrap_function_wrapper(
+ client,
+ "execute_command",
+ _traced_execute,
+ )
+ wrap_function_wrapper(
+ client,
+ "pipeline",
+ _pipeline_wrapper,
+ )
+
+
+class ValkeyInstrumentor(BaseInstrumentor):
+ """An instrumentor for Valkey.
+
+ See `BaseInstrumentor`
+ """
+
+ @staticmethod
+ def _get_tracer(**kwargs):
+ tracer_provider = kwargs.get("tracer_provider")
+ return get_tracer(
+ __name__,
+ __version__,
+ tracer_provider=tracer_provider,
+ schema_url="https://opentelemetry.io/schemas/1.11.0",
+ )
+
+ def instrument(
+ self,
+ tracer_provider: TracerProvider | None = None,
+ request_hook: Callable | None = None,
+ response_hook: Callable | None = None,
+ **kwargs,
+ ):
+ """Instruments all Valkey/StrictValkey/ValkeyCluster and async client instances.
+
+ Args:
+ tracer_provider: A TracerProvider, defaults to global.
+ request_hook:
+ a function with extra user-defined logic to run before performing the request.
+
+ The ``args`` is a tuple, where items are
+ command arguments. For example ``client.set("mykey", "value", ex=5)`` would
+ have ``args`` as ``('SET', 'mykey', 'value', 'EX', 5)``.
+
+ The ``kwargs`` represents occasional ``options`` passed by valkey. For example,
+ if you use ``client.set("mykey", "value", get=True)``, the ``kwargs`` would be
+ ``{'get': True}``.
+ response_hook:
+ a function with extra user-defined logic to run after the request is complete.
+
+ The ``args`` represents the response.
+ """
+ super().instrument(
+ tracer_provider=tracer_provider,
+ request_hook=request_hook,
+ response_hook=response_hook,
+ **kwargs,
+ )
+
+ def _instrument(self, **kwargs: Any):
+ """Instruments the valkey module
+
+ Args:
+ **kwargs: Optional arguments
+ ``tracer_provider``: a TracerProvider, defaults to global.
+ ``request_hook``: An optional callback that is invoked right after a span is created.
+ ``response_hook``: An optional callback which is invoked right before the span is finished processing a response.
+ """
+ _instrument(
+ self._get_tracer(**kwargs),
+ request_hook=kwargs.get("request_hook"),
+ response_hook=kwargs.get("response_hook"),
+ )
+
+ def _uninstrument(self, **kwargs: Any):
+ unwrap(valkey.Valkey, "execute_command")
+ unwrap(valkey.Valkey, "pipeline")
+ unwrap(valkey.client.Pipeline, "execute")
+ unwrap(valkey.client.Pipeline, "immediate_execute_command")
+ unwrap(valkey.cluster.ValkeyCluster, "execute_command")
+ unwrap(valkey.cluster.ClusterPipeline, "execute")
+ unwrap(valkey.asyncio.Valkey, "execute_command")
+ unwrap(valkey.asyncio.Valkey, "pipeline")
+ unwrap(valkey.asyncio.client.Pipeline, "execute")
+ unwrap(valkey.asyncio.client.Pipeline, "immediate_execute_command")
+ unwrap(valkey.asyncio.cluster.ValkeyCluster, "execute_command")
+ unwrap(valkey.asyncio.cluster.ClusterPipeline, "execute")
+
+ @staticmethod
+ def instrument_client(
+ client: valkey.Valkey
+ | valkey.asyncio.Valkey
+ | valkey.cluster.ValkeyCluster
+ | valkey.asyncio.ValkeyCluster,
+ tracer_provider: TracerProvider | None = None,
+ request_hook: Callable | None = None,
+ response_hook: Callable | None = None,
+ ):
+ """Instrument the provided Valkey Client. The client can be sync or async.
+ Cluster client is also supported.
+
+ Args:
+ client: The valkey client.
+ tracer_provider: A TracerProvider, defaults to global.
+ request_hook: a function with extra user-defined logic to run before
+ performing the request.
+ response_hook: a function with extra user-defined logic to run after
+ the request is complete.
+ """
+ if not hasattr(client, _INSTRUMENTATION_ATTR):
+ setattr(client, _INSTRUMENTATION_ATTR, False)
+ if not getattr(client, _INSTRUMENTATION_ATTR):
+ _instrument_client(
+ client,
+ ValkeyInstrumentor._get_tracer(
+ tracer_provider=tracer_provider
+ ),
+ request_hook=request_hook,
+ response_hook=response_hook,
+ )
+ setattr(client, _INSTRUMENTATION_ATTR, True)
+ else:
+ _logger.warning(
+ "Attempting to instrument Valkey connection while already instrumented"
+ )
+
+ @staticmethod
+ def uninstrument_client(
+ client: valkey.Valkey
+ | valkey.asyncio.Valkey
+ | valkey.cluster.ValkeyCluster
+ | valkey.asyncio.ValkeyCluster,
+ ):
+ """Disables instrumentation for the given client instance
+
+ Args:
+ client: The valkey client
+ """
+ if getattr(client, _INSTRUMENTATION_ATTR):
+ unwrap(client, "execute_command")
+ unwrap(client, "pipeline")
+ else:
+ _logger.warning(
+ "Attempting to un-instrument Valkey connection that wasn't instrumented"
+ )
+
+ def instrumentation_dependencies(self) -> Collection[str]:
+ return _instruments
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/package.py b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/package.py
new file mode 100644
index 0000000000..27a3eb9e7d
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/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 = ("valkey >= 6.0.0",)
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/py.typed b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/util.py b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/util.py
new file mode 100644
index 0000000000..cd901a8756
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/util.py
@@ -0,0 +1,57 @@
+# 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.
+#
+"""
+Utility functions for Valkey instrumentation.
+
+These functions delegate to the shared base in
+``opentelemetry.instrumentation._redis_valkey.util``, using Valkey-specific
+attribute names and conventions.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _extract_conn_attributes as _base_extract_conn_attributes,
+)
+from opentelemetry.instrumentation._redis_valkey.util import (
+ _format_command_args,
+ _set_span_attribute_if_value,
+ _value_or_none,
+)
+from opentelemetry.semconv._incubating.attributes.db_attributes import (
+ DB_REDIS_DATABASE_INDEX,
+ DB_SYSTEM,
+)
+
+_BACKEND_NAME = "valkey"
+_DB_SYSTEM = "valkey"
+_DB_SYSTEM_ATTR = DB_SYSTEM
+_DB_INDEX_ATTR = DB_REDIS_DATABASE_INDEX
+
+__all__ = [
+ "_extract_conn_attributes",
+ "_format_command_args",
+ "_set_span_attribute_if_value",
+ "_value_or_none",
+]
+
+
+def _extract_conn_attributes(conn_kwargs: dict[str, Any]) -> dict[str, Any]:
+ """Transform valkey conn info into dict."""
+ return _base_extract_conn_attributes(
+ conn_kwargs, _DB_SYSTEM, _DB_SYSTEM_ATTR, _DB_INDEX_ATTR
+ )
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/version.py b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/version.py
new file mode 100644
index 0000000000..a07bc2663e
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/src/opentelemetry/instrumentation/valkey/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.63b0.dev"
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-valkey/test-requirements.txt
new file mode 100644
index 0000000000..8fa2c63a39
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/test-requirements.txt
@@ -0,0 +1,17 @@
+asgiref==3.8.1
+async-timeout==4.0.3
+Deprecated==1.2.14
+fakeredis==2.29.0
+iniconfig==2.0.0
+packaging==24.0
+pluggy==1.5.0
+py-cpuinfo==9.0.0
+pytest==7.4.4
+valkey==6.1.0
+tomli==2.0.1
+typing_extensions==4.12.2
+wrapt==1.16.0
+zipp==3.19.2
+-e opentelemetry-instrumentation
+-e instrumentation/opentelemetry-instrumentation-redis-valkey-base
+-e instrumentation/opentelemetry-instrumentation-valkey
diff --git a/instrumentation/opentelemetry-instrumentation-valkey/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-valkey/tests/__init__.py
new file mode 100644
index 0000000000..b0a6f42841
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/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-valkey/tests/test_valkey.py b/instrumentation/opentelemetry-instrumentation-valkey/tests/test_valkey.py
new file mode 100644
index 0000000000..0ec6db86f8
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-valkey/tests/test_valkey.py
@@ -0,0 +1,293 @@
+# 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 asyncio
+from unittest import mock
+from unittest.mock import AsyncMock
+
+import valkey
+import valkey.asyncio
+
+from opentelemetry import trace
+from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
+from opentelemetry.semconv.attributes.server_attributes import (
+ SERVER_ADDRESS,
+ SERVER_PORT,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.test.test_base import TestBase
+from opentelemetry.trace import SpanKind
+
+
+class TestValkey(TestBase):
+ def setUp(self):
+ super().setUp()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ def tearDown(self):
+ super().tearDown()
+ ValkeyInstrumentor().uninstrument()
+
+ def test_span_properties(self):
+ valkey_client = valkey.Valkey()
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_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)
+
+ def test_not_recording(self):
+ valkey_client = valkey.Valkey()
+
+ 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(valkey_client, "connection"):
+ tracer.return_value = mock_tracer
+ valkey_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)
+
+ def test_instrument_uninstrument(self):
+ valkey_client = valkey.Valkey()
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.get("key")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ self.memory_exporter.clear()
+
+ # Test uninstrument
+ ValkeyInstrumentor().uninstrument()
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.get("key")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 0)
+ self.memory_exporter.clear()
+
+ # Test instrument again
+ ValkeyInstrumentor().instrument()
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.get("key")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ def test_instrument_uninstrument_async_client_command(self):
+ valkey_client = valkey.asyncio.Valkey()
+
+ with mock.patch.object(valkey_client, "connection", AsyncMock()):
+ asyncio.run(valkey_client.get("key"))
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ self.memory_exporter.clear()
+
+ # Test uninstrument
+ ValkeyInstrumentor().uninstrument()
+
+ with mock.patch.object(valkey_client, "connection", AsyncMock()):
+ asyncio.run(valkey_client.get("key"))
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 0)
+ self.memory_exporter.clear()
+
+ # Test instrument again
+ ValkeyInstrumentor().instrument()
+
+ with mock.patch.object(valkey_client, "connection", AsyncMock()):
+ asyncio.run(valkey_client.get("key"))
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ def test_response_hook(self):
+ valkey_client = valkey.Valkey()
+ connection = valkey.connection.Connection()
+ valkey_client.connection = connection
+
+ response_attribute_name = "db.valkey.response"
+
+ def response_hook(span, conn, response):
+ span.set_attribute(response_attribute_name, response)
+
+ ValkeyInstrumentor().uninstrument()
+ ValkeyInstrumentor().instrument(
+ tracer_provider=self.tracer_provider, response_hook=response_hook
+ )
+
+ test_value = "test_value"
+
+ with mock.patch.object(connection, "send_command"):
+ with mock.patch.object(
+ valkey_client, "parse_response", return_value=test_value
+ ):
+ valkey_client.get("key")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+ self.assertEqual(
+ span.attributes.get(response_attribute_name), test_value
+ )
+
+ def test_request_hook(self):
+ valkey_client = valkey.Valkey()
+ connection = valkey.connection.Connection()
+ valkey_client.connection = connection
+
+ custom_attribute_name = "my.request.attribute"
+
+ def request_hook(span, conn, args, kwargs):
+ if span and span.is_recording():
+ span.set_attribute(custom_attribute_name, args[0])
+
+ ValkeyInstrumentor().uninstrument()
+ ValkeyInstrumentor().instrument(
+ tracer_provider=self.tracer_provider, request_hook=request_hook
+ )
+
+ test_value = "test_value"
+
+ with mock.patch.object(connection, "send_command"):
+ with mock.patch.object(
+ valkey_client, "parse_response", return_value=test_value
+ ):
+ valkey_client.get("key")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+ self.assertEqual(span.attributes.get(custom_attribute_name), "GET")
+
+ def test_query_sanitizer_enabled(self):
+ valkey_client = valkey.Valkey()
+ connection = valkey.connection.Connection()
+ valkey_client.connection = connection
+
+ ValkeyInstrumentor().uninstrument()
+ ValkeyInstrumentor().instrument(
+ tracer_provider=self.tracer_provider,
+ sanitize_query=True,
+ )
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.set("key", "value")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+ self.assertEqual(span.attributes.get("db.statement"), "SET ? ?")
+
+ def test_query_sanitizer(self):
+ valkey_client = valkey.Valkey()
+ connection = valkey.connection.Connection()
+ valkey_client.connection = connection
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.set("key", "value")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+ self.assertEqual(span.attributes.get("db.statement"), "SET ? ?")
+
+ def test_no_op_tracer_provider(self):
+ ValkeyInstrumentor().uninstrument()
+ tracer_provider = trace.NoOpTracerProvider()
+ ValkeyInstrumentor().instrument(tracer_provider=tracer_provider)
+
+ valkey_client = valkey.Valkey()
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.get("key")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 0)
+
+ def test_attributes_default(self):
+ valkey_client = valkey.Valkey()
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.set("key", "value")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+ self.assertEqual(
+ span.attributes[SpanAttributes.DB_SYSTEM],
+ "valkey",
+ )
+ self.assertEqual(span.attributes["db.redis.database_index"], 0)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 6379)
+
+ def test_attributes_tcp(self):
+ valkey_client = valkey.Valkey.from_url(
+ "valkey://foo:bar@1.1.1.1:6380/1"
+ )
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.set("key", "value")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+ self.assertEqual(
+ span.attributes[SpanAttributes.DB_SYSTEM],
+ "valkey",
+ )
+ self.assertEqual(span.attributes["db.redis.database_index"], 1)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "1.1.1.1")
+ self.assertEqual(span.attributes[SERVER_PORT], 6380)
+
+ def test_attributes_unix_socket(self):
+ valkey_client = valkey.Valkey.from_url(
+ "unix://foo@/path/to/socket.sock?db=3&password=bar"
+ )
+
+ with mock.patch.object(valkey_client, "connection"):
+ valkey_client.set("key", "value")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+ self.assertEqual(
+ span.attributes[SpanAttributes.DB_SYSTEM],
+ "valkey",
+ )
+ self.assertEqual(span.attributes["db.redis.database_index"], 3)
+ self.assertEqual(
+ span.attributes[SERVER_ADDRESS],
+ "/path/to/socket.sock",
+ )
diff --git a/opentelemetry-contrib-instrumentations/pyproject.toml b/opentelemetry-contrib-instrumentations/pyproject.toml
index 630509a4b2..a254e9666d 100644
--- a/opentelemetry-contrib-instrumentations/pyproject.toml
+++ b/opentelemetry-contrib-instrumentations/pyproject.toml
@@ -67,6 +67,7 @@ dependencies = [
"opentelemetry-instrumentation-pymysql==0.63b0.dev",
"opentelemetry-instrumentation-pyramid==0.63b0.dev",
"opentelemetry-instrumentation-redis==0.63b0.dev",
+ "opentelemetry-instrumentation-redis-valkey-base==0.63b0.dev",
"opentelemetry-instrumentation-remoulade==0.63b0.dev",
"opentelemetry-instrumentation-requests==0.63b0.dev",
"opentelemetry-instrumentation-sqlalchemy==0.63b0.dev",
@@ -78,6 +79,7 @@ dependencies = [
"opentelemetry-instrumentation-tortoiseorm==0.63b0.dev",
"opentelemetry-instrumentation-urllib==0.63b0.dev",
"opentelemetry-instrumentation-urllib3==0.63b0.dev",
+ "opentelemetry-instrumentation-valkey==0.63b0.dev",
"opentelemetry-instrumentation-wsgi==0.63b0.dev",
]
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
index 7f38e43c96..9fc284151a 100644
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
@@ -201,11 +201,16 @@
"library": "urllib3 >= 1.0.0, < 3.0.0",
"instrumentation": "opentelemetry-instrumentation-urllib3==0.63b0.dev",
},
+ {
+ "library": "valkey[libvalkey] >= 6.1.0",
+ "instrumentation": "opentelemetry-instrumentation-valkey==0.63b0.dev",
+ },
]
default_instrumentations = [
"opentelemetry-instrumentation-asyncio==0.63b0.dev",
"opentelemetry-instrumentation-dbapi==0.63b0.dev",
"opentelemetry-instrumentation-logging==0.63b0.dev",
+ "opentelemetry-instrumentation-redis-valkey-base==0.63b0.dev",
"opentelemetry-instrumentation-sqlite3==0.63b0.dev",
"opentelemetry-instrumentation-threading==0.63b0.dev",
"opentelemetry-instrumentation-urllib==0.63b0.dev",
diff --git a/pyproject.toml b/pyproject.toml
index 8f8f26855c..2b4960ae08 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -60,6 +60,7 @@ dependencies = [
"opentelemetry-instrumentation-tortoiseorm[instruments]",
"opentelemetry-instrumentation-urllib",
"opentelemetry-instrumentation-urllib3[instruments]",
+ "opentelemetry-instrumentation-valkey[instruments]",
"opentelemetry-instrumentation-wsgi",
"opentelemetry-propagator-ot-trace",
"opentelemetry-propagator-aws-xray",
@@ -122,6 +123,7 @@ opentelemetry-instrumentation-pymongo = { workspace = true }
opentelemetry-instrumentation-pymysql = { workspace = true }
opentelemetry-instrumentation-pyramid = { workspace = true }
opentelemetry-instrumentation-redis = { workspace = true }
+opentelemetry-instrumentation-redis-valkey-base = { workspace = true }
opentelemetry-instrumentation-remoulade = { workspace = true }
opentelemetry-instrumentation-requests = { workspace = true }
opentelemetry-instrumentation-sqlalchemy = { workspace = true }
@@ -132,6 +134,7 @@ opentelemetry-instrumentation-tornado = { workspace = true }
opentelemetry-instrumentation-tortoiseorm = { workspace = true }
opentelemetry-instrumentation-urllib = { workspace = true }
opentelemetry-instrumentation-urllib3 = { workspace = true }
+opentelemetry-instrumentation-valkey = { workspace = true }
opentelemetry-instrumentation-wsgi = { workspace = true }
opentelemetry-propagator-ot-trace = { workspace = true }
opentelemetry-propagator-aws-xray = { workspace = true }
diff --git a/tests/opentelemetry-docker-tests/tests/check_availability.py b/tests/opentelemetry-docker-tests/tests/check_availability.py
index 2d1d488d55..2055c46063 100644
--- a/tests/opentelemetry-docker-tests/tests/check_availability.py
+++ b/tests/opentelemetry-docker-tests/tests/check_availability.py
@@ -9,6 +9,7 @@
import pymongo
import pyodbc
import redis
+import valkey
MONGODB_COLLECTION_NAME = "test"
MONGODB_DB_NAME = os.getenv("MONGODB_DB_NAME", "opentelemetry-tests")
@@ -26,6 +27,8 @@
POSTGRES_USER = os.getenv("POSTGRESQL_USER", "testuser")
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT ", "6379"))
+VALKEY_HOST = os.getenv("VALKEY_HOST", "localhost")
+VALKEY_PORT = int(os.getenv("VALKEY_PORT ", "16379"))
MSSQL_DB_NAME = os.getenv("MSSQL_DB_NAME", "opentelemetry-tests")
MSSQL_HOST = os.getenv("MSSQL_HOST", "localhost")
MSSQL_PORT = int(os.getenv("MSSQL_PORT", "1433"))
@@ -101,6 +104,12 @@ def check_redis_connection():
connection.hgetall("*")
+@retryable
+def check_valkey_connection():
+ connection = valkey.Valkey(host=VALKEY_HOST, port=VALKEY_PORT)
+ connection.hgetall("*")
+
+
def new_mssql_connection() -> pyodbc.Connection:
connection = pyodbc.connect(
f"DRIVER={{ODBC Driver 18 for SQL Server}};SERVER={MSSQL_HOST},"
@@ -130,6 +139,7 @@ def check_docker_services_availability():
check_mysql_connection()
check_postgres_connection()
check_redis_connection()
+ check_valkey_connection()
check_mssql_connection()
setup_mssql_db()
diff --git a/tests/opentelemetry-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml
index 02a3721d9b..92c4a5fdbe 100644
--- a/tests/opentelemetry-docker-tests/tests/docker-compose.yml
+++ b/tests/opentelemetry-docker-tests/tests/docker-compose.yml
@@ -38,6 +38,11 @@ services:
- "127.0.0.1:7003:7003"
- "127.0.0.1:7004:7004"
- "127.0.0.1:7005:7005"
+ otvalkey:
+ image: valkey/valkey:8.1.1
+ ports:
+ - "127.0.0.1:16379:6379"
+
otjaeger:
image: jaegertracing/all-in-one:1.8
environment:
diff --git a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py
index 1f6e370bc6..4600a218e6 100644
--- a/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py
@@ -20,9 +20,9 @@
DB_REDIS_DATABASE_INDEX,
DB_STATEMENT,
)
-from opentelemetry.semconv._incubating.attributes.net_attributes import (
- NET_PEER_NAME,
- NET_PEER_PORT,
+from opentelemetry.semconv.attributes.server_attributes import (
+ SERVER_ADDRESS,
+ SERVER_PORT,
)
from opentelemetry.test.test_base import TestBase
@@ -42,8 +42,8 @@ def _check_span(self, span, name):
self.assertEqual(span.name, name)
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
self.assertEqual(span.attributes.get(DB_REDIS_DATABASE_INDEX), 0)
- self.assertEqual(span.attributes[NET_PEER_NAME], "localhost")
- self.assertEqual(span.attributes[NET_PEER_PORT], 6379)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 6379)
def test_long_command_sanitized(self):
RedisInstrumentor().uninstrument()
@@ -266,8 +266,8 @@ def _check_span(self, span, name):
self.assertEqual(span.name, name)
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
self.assertEqual(span.attributes.get(DB_REDIS_DATABASE_INDEX), 0)
- self.assertEqual(span.attributes[NET_PEER_NAME], "localhost")
- self.assertEqual(span.attributes[NET_PEER_PORT], 6379)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 6379)
def test_long_command(self):
async_call(self.redis_client.mget(*range(1000)))
@@ -564,8 +564,8 @@ def tearDown(self):
def _check_span(self, span, name):
self.assertEqual(span.name, name)
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
- self.assertEqual(span.attributes[NET_PEER_NAME], "localhost")
- self.assertEqual(span.attributes[NET_PEER_PORT], 6379)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 6379)
self.assertEqual(span.attributes[DB_REDIS_DATABASE_INDEX], 10)
def test_get(self):
diff --git a/tests/opentelemetry-docker-tests/tests/test-requirements.txt b/tests/opentelemetry-docker-tests/tests/test-requirements.txt
index 4b6c61fba5..1e2110ff9f 100644
--- a/tests/opentelemetry-docker-tests/tests/test-requirements.txt
+++ b/tests/opentelemetry-docker-tests/tests/test-requirements.txt
@@ -66,6 +66,7 @@ tomli==2.0.1
typing_extensions==4.12.2
tzdata==2024.1
urllib3==1.26.19
+valkey[libvalkey]==6.1.0
vine==5.1.0
wcwidth==0.2.13
websocket-client==0.59.0
diff --git a/tests/opentelemetry-docker-tests/tests/valkey/test_valkey_functional.py b/tests/opentelemetry-docker-tests/tests/valkey/test_valkey_functional.py
new file mode 100644
index 0000000000..48fe167f46
--- /dev/null
+++ b/tests/opentelemetry-docker-tests/tests/valkey/test_valkey_functional.py
@@ -0,0 +1,608 @@
+# 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 asyncio
+from time import time_ns
+
+import valkey
+import valkey.asyncio
+
+from opentelemetry import trace
+from opentelemetry.instrumentation.valkey import ValkeyInstrumentor
+from opentelemetry.semconv.attributes.server_attributes import (
+ SERVER_ADDRESS,
+ SERVER_PORT,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.test.test_base import TestBase
+
+
+class TestValkeyInstrument(TestBase):
+ def setUp(self):
+ super().setUp()
+ self.valkey_client = valkey.Valkey(port=16379)
+ self.valkey_client.flushall()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ def tearDown(self):
+ ValkeyInstrumentor().uninstrument()
+ super().tearDown()
+
+ def _check_span(self, span, name):
+ self.assertEqual(span.name, name)
+ self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
+ self.assertEqual(span.attributes.get("db.redis.database_index"), 0)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 16379)
+
+ def test_long_command_sanitized(self):
+ ValkeyInstrumentor().uninstrument()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ self.valkey_client.mget(*range(2000))
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "MGET")
+ self.assertTrue(
+ span.attributes.get(SpanAttributes.DB_STATEMENT).startswith(
+ "MGET ? ? ? ?"
+ )
+ )
+ self.assertTrue(
+ span.attributes.get(SpanAttributes.DB_STATEMENT).endswith("...")
+ )
+
+ def test_long_command(self):
+ self.valkey_client.mget(*range(1000))
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "MGET")
+ self.assertTrue(
+ span.attributes.get(SpanAttributes.DB_STATEMENT).startswith(
+ "MGET ? ? ? ?"
+ )
+ )
+ self.assertTrue(
+ span.attributes.get(SpanAttributes.DB_STATEMENT).endswith("...")
+ )
+
+ def test_basics_sanitized(self):
+ ValkeyInstrumentor().uninstrument()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ self.assertIsNone(self.valkey_client.get("cheese"))
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "GET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
+ )
+ self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
+
+ def test_basics(self):
+ self.assertIsNone(self.valkey_client.get("cheese"))
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "GET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
+ )
+ self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
+
+ def test_pipeline_traced_sanitized(self):
+ ValkeyInstrumentor().uninstrument()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ with self.valkey_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, "SET RPUSH HGETALL")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT),
+ "SET ? ?\nRPUSH ? ?\nHGETALL ?",
+ )
+ self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
+
+ def test_pipeline_traced(self):
+ with self.valkey_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, "SET RPUSH HGETALL")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT),
+ "SET ? ?\nRPUSH ? ?\nHGETALL ?",
+ )
+ self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
+
+ def test_pipeline_immediate_sanitized(self):
+ ValkeyInstrumentor().uninstrument()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ with self.valkey_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, "SET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "SET ? ?"
+ )
+
+ def test_pipeline_immediate(self):
+ with self.valkey_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, "SET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "SET ? ?"
+ )
+
+ def test_parent(self):
+ """Ensure OpenTelemetry works with valkey."""
+ ot_tracer = trace.get_tracer("valkey_svc")
+
+ with ot_tracer.start_as_current_span("valkey_get"):
+ self.assertIsNone(self.valkey_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_span_context())
+
+ self.assertEqual(parent_span.name, "valkey_get")
+ self.assertEqual(parent_span.instrumentation_info.name, "valkey_svc")
+
+ self.assertEqual(child_span.name, "GET")
+
+
+class TestValkeyClusterInstrument(TestBase):
+ def setUp(self):
+ super().setUp()
+ self.valkey_client = valkey.cluster.ValkeyCluster(
+ host="localhost", port=7000
+ )
+ self.valkey_client.flushall()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ def tearDown(self):
+ super().tearDown()
+ ValkeyInstrumentor().uninstrument()
+
+ def _check_span(self, span, name):
+ self.assertEqual(span.name, name)
+ self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
+
+ def test_basics(self):
+ self.assertIsNone(self.valkey_client.get("cheese"))
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "GET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
+ )
+ self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
+
+ def test_pipeline_traced(self):
+ with self.valkey_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, "SET RPUSH HGETALL")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT),
+ "SET ? ?\nRPUSH ? ?\nHGETALL ?",
+ )
+ self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
+
+ def test_parent(self):
+ """Ensure OpenTelemetry works with valkey."""
+ ot_tracer = trace.get_tracer("valkey_svc")
+
+ with ot_tracer.start_as_current_span("valkey_get"):
+ self.assertIsNone(self.valkey_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_span_context())
+
+ self.assertEqual(parent_span.name, "valkey_get")
+ self.assertEqual(parent_span.instrumentation_info.name, "valkey_svc")
+
+ self.assertEqual(child_span.name, "GET")
+
+
+def async_call(coro):
+ loop = asyncio.get_event_loop()
+ return loop.run_until_complete(coro)
+
+
+class TestAsyncValkeyInstrument(TestBase):
+ def setUp(self):
+ super().setUp()
+ self.valkey_client = valkey.asyncio.Valkey(port=16379)
+ async_call(self.valkey_client.flushall())
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ def tearDown(self):
+ ValkeyInstrumentor().uninstrument()
+ super().tearDown()
+
+ def _check_span(self, span, name):
+ self.assertEqual(span.name, name)
+ self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
+ self.assertEqual(span.attributes.get("db.redis.database_index"), 0)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 16379)
+
+ def test_long_command(self):
+ async_call(self.valkey_client.mget(*range(1000)))
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "MGET")
+ self.assertTrue(
+ span.attributes.get(SpanAttributes.DB_STATEMENT).startswith(
+ "MGET ? ? ? ?"
+ )
+ )
+ self.assertTrue(
+ span.attributes.get(SpanAttributes.DB_STATEMENT).endswith("...")
+ )
+
+ def test_basics(self):
+ self.assertIsNone(async_call(self.valkey_client.get("cheese")))
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "GET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
+ )
+ self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
+
+ def test_execute_command_traced_full_time(self):
+ """Command should be traced for coroutine execution time, not creation time."""
+ coro_created_time = None
+ finish_time = None
+
+ async def pipeline_simple():
+ nonlocal coro_created_time
+ nonlocal finish_time
+
+ # delay coroutine creation from coroutine execution
+ coro = self.valkey_client.get("foo")
+ coro_created_time = time_ns()
+ await coro
+ finish_time = time_ns()
+
+ async_call(pipeline_simple())
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self.assertTrue(span.start_time > coro_created_time)
+ self.assertTrue(span.end_time < finish_time)
+
+ def test_pipeline_traced(self):
+ async def pipeline_simple():
+ async with self.valkey_client.pipeline(
+ transaction=False
+ ) as pipeline:
+ pipeline.set("blah", 32)
+ pipeline.rpush("foo", "éé")
+ pipeline.hgetall("xxx")
+ await pipeline.execute()
+
+ async_call(pipeline_simple())
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "SET RPUSH HGETALL")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT),
+ "SET ? ?\nRPUSH ? ?\nHGETALL ?",
+ )
+ self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
+
+ def test_pipeline_traced_full_time(self):
+ """Command should be traced for coroutine execution time, not creation time."""
+ coro_created_time = None
+ finish_time = None
+
+ async def pipeline_simple():
+ async with self.valkey_client.pipeline(
+ transaction=False
+ ) as pipeline:
+ nonlocal coro_created_time
+ nonlocal finish_time
+ pipeline.set("blah", 32)
+ pipeline.rpush("foo", "éé")
+ pipeline.hgetall("xxx")
+
+ # delay coroutine creation from coroutine execution
+ coro = pipeline.execute()
+ coro_created_time = time_ns()
+ await coro
+ finish_time = time_ns()
+
+ async_call(pipeline_simple())
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self.assertTrue(span.start_time > coro_created_time)
+ self.assertTrue(span.end_time < finish_time)
+
+ def test_pipeline_immediate(self):
+ async def pipeline_immediate():
+ async with self.valkey_client.pipeline() as pipeline:
+ pipeline.set("a", 1)
+ await pipeline.immediate_execute_command("SET", "b", 2)
+ await pipeline.execute()
+
+ async_call(pipeline_immediate())
+
+ 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, "SET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "SET ? ?"
+ )
+
+ def test_pipeline_immediate_traced_full_time(self):
+ """Command should be traced for coroutine execution time, not creation time."""
+ coro_created_time = None
+ finish_time = None
+
+ async def pipeline_simple():
+ async with self.valkey_client.pipeline(
+ transaction=False
+ ) as pipeline:
+ nonlocal coro_created_time
+ nonlocal finish_time
+ pipeline.set("a", 1)
+
+ # delay coroutine creation from coroutine execution
+ coro = pipeline.immediate_execute_command("SET", "b", 2)
+ coro_created_time = time_ns()
+ await coro
+ finish_time = time_ns()
+
+ async_call(pipeline_simple())
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self.assertTrue(span.start_time > coro_created_time)
+ self.assertTrue(span.end_time < finish_time)
+
+ def test_parent(self):
+ """Ensure OpenTelemetry works with valkey."""
+ ot_tracer = trace.get_tracer("valkey_svc")
+
+ with ot_tracer.start_as_current_span("valkey_get"):
+ self.assertIsNone(async_call(self.valkey_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_span_context())
+
+ self.assertEqual(parent_span.name, "valkey_get")
+ self.assertEqual(parent_span.instrumentation_info.name, "valkey_svc")
+
+ self.assertEqual(child_span.name, "GET")
+
+
+class TestAsyncValkeyClusterInstrument(TestBase):
+ def setUp(self):
+ super().setUp()
+ self.valkey_client = valkey.asyncio.cluster.ValkeyCluster(
+ host="localhost", port=7000
+ )
+ async_call(self.valkey_client.flushall())
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ def tearDown(self):
+ super().tearDown()
+ ValkeyInstrumentor().uninstrument()
+
+ def _check_span(self, span, name):
+ self.assertEqual(span.name, name)
+ self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
+
+ def test_basics(self):
+ self.assertIsNone(async_call(self.valkey_client.get("cheese")))
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "GET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
+ )
+ self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
+
+ def test_execute_command_traced_full_time(self):
+ """Command should be traced for coroutine execution time, not creation time."""
+ coro_created_time = None
+ finish_time = None
+
+ async def pipeline_simple():
+ nonlocal coro_created_time
+ nonlocal finish_time
+
+ # delay coroutine creation from coroutine execution
+ coro = self.valkey_client.get("foo")
+ coro_created_time = time_ns()
+ await coro
+ finish_time = time_ns()
+
+ async_call(pipeline_simple())
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self.assertTrue(span.start_time > coro_created_time)
+ self.assertTrue(span.end_time < finish_time)
+
+ def test_pipeline_traced(self):
+ async def pipeline_simple():
+ async with self.valkey_client.pipeline(
+ transaction=False
+ ) as pipeline:
+ pipeline.set("blah", 32)
+ pipeline.rpush("foo", "éé")
+ pipeline.hgetall("xxx")
+ await pipeline.execute()
+
+ async_call(pipeline_simple())
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "SET RPUSH HGETALL")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT),
+ "SET ? ?\nRPUSH ? ?\nHGETALL ?",
+ )
+ self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
+
+ def test_pipeline_traced_full_time(self):
+ """Command should be traced for coroutine execution time, not creation time."""
+ coro_created_time = None
+ finish_time = None
+
+ async def pipeline_simple():
+ async with self.valkey_client.pipeline(
+ transaction=False
+ ) as pipeline:
+ nonlocal coro_created_time
+ nonlocal finish_time
+ pipeline.set("blah", 32)
+ pipeline.rpush("foo", "éé")
+ pipeline.hgetall("xxx")
+
+ # delay coroutine creation from coroutine execution
+ coro = pipeline.execute()
+ coro_created_time = time_ns()
+ await coro
+ finish_time = time_ns()
+
+ async_call(pipeline_simple())
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self.assertTrue(span.start_time > coro_created_time)
+ self.assertTrue(span.end_time < finish_time)
+
+ def test_parent(self):
+ """Ensure OpenTelemetry works with valkey."""
+ ot_tracer = trace.get_tracer("valkey_svc")
+
+ with ot_tracer.start_as_current_span("valkey_get"):
+ self.assertIsNone(async_call(self.valkey_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_span_context())
+
+ self.assertEqual(parent_span.name, "valkey_get")
+ self.assertEqual(parent_span.instrumentation_info.name, "valkey_svc")
+
+ self.assertEqual(child_span.name, "GET")
+
+
+class TestValkeyDBIndexInstrument(TestBase):
+ def setUp(self):
+ super().setUp()
+ self.valkey_client = valkey.Valkey(port=16379, db=10)
+ self.valkey_client.flushall()
+ ValkeyInstrumentor().instrument(tracer_provider=self.tracer_provider)
+
+ def tearDown(self):
+ ValkeyInstrumentor().uninstrument()
+ super().tearDown()
+
+ def _check_span(self, span, name):
+ self.assertEqual(span.name, name)
+ self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
+ self.assertEqual(span.attributes[SERVER_ADDRESS], "localhost")
+ self.assertEqual(span.attributes[SERVER_PORT], 16379)
+ self.assertEqual(span.attributes["db.redis.database_index"], 10)
+
+ def test_get(self):
+ self.assertIsNone(self.valkey_client.get("foo"))
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+ span = spans[0]
+ self._check_span(span, "GET")
+ self.assertEqual(
+ span.attributes.get(SpanAttributes.DB_STATEMENT), "GET ?"
+ )
diff --git a/tox.ini b/tox.ini
index 897a1c75d2..1f4df7c525 100644
--- a/tox.ini
+++ b/tox.ini
@@ -439,6 +439,11 @@ envlist =
; requires snappy headers to be available on the system
lint-processor-baggage
+ ; opentelemetry-instrumentation-valkey
+ py3{10,11,12,13,14}-test-instrumentation-valkey
+ pypy3-test-instrumentation-valkey
+ lint-instrumentation-valkey
+
; opentelemetry-opamp-client
py3{10,11,12,13,14}-test-opamp-client-{latest,lowest}
; https://github.com/kevin1024/vcrpy/pull/775#issuecomment-1847849962
@@ -667,6 +672,9 @@ deps =
redis: {[testenv]test_deps}
redis: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt
+ valkey: {[testenv]test_deps}
+ valkey: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-valkey/test-requirements.txt
+
remoulade: {[testenv]test_deps}
remoulade: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/test-requirements.txt
@@ -954,6 +962,9 @@ commands =
test-instrumentation-redis: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-redis/tests {posargs}
lint-instrumentation-redis: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-redis"
+ test-instrumentation-valkey: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-valkey/tests {posargs}
+ lint-instrumentation-valkey: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-valkey"
+
test-instrumentation-remoulade: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-remoulade/tests {posargs}
lint-instrumentation-remoulade: sh -c "cd instrumentation && pylint --rcfile ../.pylintrc opentelemetry-instrumentation-remoulade"
@@ -1075,7 +1086,9 @@ deps =
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-pymssql
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-sqlalchemy
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-aiopg
+ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis-valkey-base
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-redis
+ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-valkey
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi
-e {toxinidir}/util/opentelemetry-util-http
diff --git a/uv.lock b/uv.lock
index 14d5ea35b6..b32bd2679f 100644
--- a/uv.lock
+++ b/uv.lock
@@ -60,6 +60,7 @@ members = [
"opentelemetry-instrumentation-pymysql",
"opentelemetry-instrumentation-pyramid",
"opentelemetry-instrumentation-redis",
+ "opentelemetry-instrumentation-redis-valkey-base",
"opentelemetry-instrumentation-remoulade",
"opentelemetry-instrumentation-requests",
"opentelemetry-instrumentation-sqlalchemy",
@@ -71,6 +72,7 @@ members = [
"opentelemetry-instrumentation-tortoiseorm",
"opentelemetry-instrumentation-urllib",
"opentelemetry-instrumentation-urllib3",
+ "opentelemetry-instrumentation-valkey",
"opentelemetry-instrumentation-vertexai",
"opentelemetry-instrumentation-weaviate",
"opentelemetry-instrumentation-wsgi",
@@ -2284,6 +2286,80 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/7e/e7394eeb49a41cc514b3eb49020223666cbf40d86f5721c2f07871e6d84a/legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd", size = 20035, upload-time = "2025-10-27T05:20:04.289Z" },
]
+[[package]]
+name = "libvalkey"
+version = "4.0.1"
+source = { registry = "https://pypi.org/simple/" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/49/57857ba9d02ba4df3bc1e71044f599c82ea590e928328e6b512dbf720228/libvalkey-4.0.1.tar.gz", hash = "sha256:fe60ef535bc826fc35f4019228a0a46bdce8b41fd6013a7591e822a8a17c3170", size = 109005, upload-time = "2024-11-26T14:35:38.504Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/ac/c7b21f810c17527f77c8bd4145e21568a95a4eccc32bce8df4c5ba5d8863/libvalkey-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e180a893ac62e340a63e18c6dcc91fc756c9f2291b47b35ee1febec68c6d13c", size = 43044, upload-time = "2024-11-26T14:32:50.692Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/67/84c88cf0e8df1d68da9a2e7f6a79e818031a7ced1b70d66c60041e8dd7b5/libvalkey-4.0.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:f51c08cae774071ea354658f9a9bb7ffb7b1743661011a28668650b130e0d063", size = 82094, upload-time = "2024-11-26T14:32:52.855Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b3/4a7bf5a0275674cb17e0204c05e16356c94406256d811317e6ba87e1f234/libvalkey-4.0.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:98a37d1cb5f4c3dde6646968b0a624d3051fd99176583a5245641050e931a682", size = 44753, upload-time = "2024-11-26T14:32:54.355Z" },
+ { url = "https://files.pythonhosted.org/packages/25/2f/aee00783de3ad3fbbadca23692f854f4b9f4b545c55db19657a4330e1bb4/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01c4051c3b772bd3032ca66c96f229412622ef0bef344f9ad645221f56082573", size = 170126, upload-time = "2024-11-26T14:32:55.608Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/56/c8f80d3fa881cf79d20b537dcbc3ab5c8776db51cfa50b2a04a3191d027d/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf424fe1f45462ae4fea5f88b250ae86d7217a9662cfe5cd8a25208268129833", size = 165552, upload-time = "2024-11-26T14:32:56.936Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/0c/e697bc18740d95dd0a3104404a0fbaaf4f1d463ac6b7ed2f6fc0c8be4b6a/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea9bdd8fc54de6ceea9e28dc28b327a443423dd1d110bb9fa1d67f02626a8679", size = 181753, upload-time = "2024-11-26T14:32:58.239Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/49e0b1eb0cc9aee67d711386cbda604ea67752d17d9f94b62ea9866716af/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:641eed2e36408b8ba553c4606b2cfbee05286183249c76841188897d595c6639", size = 170657, upload-time = "2024-11-26T14:32:59.516Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/07/205b2e63936f880d0ff8cc7cc27059aa698a0d4f02dc9194854eadce985c/libvalkey-4.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbf76b1b51bd5fe23dd09d0b7599cf6ee7a074e73a1933910e5faa1741408708", size = 170331, upload-time = "2024-11-26T14:33:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/35/93/31734676641cb36a3ac23ffedc89b328a63a2f00eba80fc0554e2dd93872/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:75918faf78b2728ef8a73438361c328d70757cdb0e8bf57fa636e0776f302d4e", size = 164756, upload-time = "2024-11-26T14:33:03.974Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/fd/1f8476e45792c1bd6bb364e7f04c0274caa37767d4ed12658b1d863874cf/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:aa678090591e1c28a5f0647ce69531752e8f75e83a03e8963500941475898ccf", size = 162825, upload-time = "2024-11-26T14:33:05.273Z" },
+ { url = "https://files.pythonhosted.org/packages/23/03/c293870a74b88d8ce2c5fed2a049eb2ec2373e29ccd3eb3df32217746e00/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a1eff0939e0577ddc6b8b359a846c0a83cb7ed3b0688fe98f8f8cf3ba8aa04b7", size = 176181, upload-time = "2024-11-26T14:33:06.637Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/b0/8624fa9195c4af93044860c9685b7667c1b7dd9bff6b62f815e65a512f24/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b3ac608744fc2727eb87cdd7613f8d64b18a210b1661706d2b2de09fffd3d2d0", size = 167755, upload-time = "2024-11-26T14:33:08.033Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/66/74f722d90e37addf2b1235ce8294d50751c1a406a7dd05e7b4893f474daa/libvalkey-4.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8801cf0274b2a6b0d19ea47de351e5ce67579b8503c4f9905ab53b52fbf270e", size = 165462, upload-time = "2024-11-26T14:33:10.035Z" },
+ { url = "https://files.pythonhosted.org/packages/18/a1/16512251a897ad7022787ae395c2ecaf48449f7fce170d6656c5a27df795/libvalkey-4.0.1-cp310-cp310-win32.whl", hash = "sha256:a9438415f500c1b65fe258f102b004ef690db142a74d681d10fd82e344dc947f", size = 19468, upload-time = "2024-11-26T14:33:11.446Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/a8/7819672c42b470c67a994db05dd876b88ad97d5e4ae3152a7e49172b0b1b/libvalkey-4.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:cd3495a5c4c7f04c26bd5c34feb15c13da2dab5a349756a3f42f2a15521a5197", size = 21354, upload-time = "2024-11-26T14:33:12.635Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7a/d7e4726c9a08c703fd4e824c7c644ae6c5f0ee3f1b99474a524f0149b77f/libvalkey-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f342da7200765da30e8a6a540722a8e9e689b0b0604e067290d308981d93826f", size = 43043, upload-time = "2024-11-26T14:33:13.78Z" },
+ { url = "https://files.pythonhosted.org/packages/27/ae/30ba1da48e143da9c1fa0e4cf4899bbd4402b0a0c8f6c355aa76e89b6490/libvalkey-4.0.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:0db70261f8843007ea995f7adf0d619780380ac3abc4c1ac44ad4f3e885d5594", size = 82093, upload-time = "2024-11-26T14:33:15.517Z" },
+ { url = "https://files.pythonhosted.org/packages/84/da/1c2b524ad44a7c24d7e62bb63d995bb8e59863c0f88844778c109604f83f/libvalkey-4.0.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:785c73ba7177777d9af01f48aa3344099815cdae3fef12c5d0f35b9b392f35bf", size = 44754, upload-time = "2024-11-26T14:33:16.656Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/1f/954f1ac80371dc50efaada4ca133219e2edb265c136c7fa2af1821caf5b2/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d56ed4c6c17bfb65bf4fe0745fdb3ae6bd1111af6171294497173e3a45226d7", size = 170152, upload-time = "2024-11-26T14:33:17.841Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/3f/1fbe055702041bcf2a85e056955219102e8b0aa3967482a5d68f7a3bb8c8/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d99b4adae993b9d8f057e150e5a2f938823d17286abcbc5cca0cb4741c530ddf", size = 165334, upload-time = "2024-11-26T14:33:19.504Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ad/ef273ef578fbac6e3b336c2138e457893654da0a40c75fcc76bf28be4761/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d803812b4933a1926479aff4057f06b4332977b796038b4309d98546c56edf5", size = 181816, upload-time = "2024-11-26T14:33:21.329Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/87/3b681951477e98e135dee6f8b570779875a96a0367ad886327610f9134ee/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b473dc3d005a9b57e4445cb2a9ab48f8a26ea90889458ef3cb4d3dd7b23b5a26", size = 170687, upload-time = "2024-11-26T14:33:22.596Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4c/d384395d2378e6481cdcb1fb7c6e6d0a3ae0feb3dffaefbd6c1eb39cb3d1/libvalkey-4.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:195f7e78cb6d2c391dac2d0fc1bf7e65555ebad856e0c36bcc4986e0b3b6c616", size = 170453, upload-time = "2024-11-26T14:33:24.791Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d8/1c64a5704ef4f843e2ffab8d815f1c918445f92e38660a6d44c7c64d1fe1/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79b446abdb18aefc984214de68ac5f50164550a00b703b81c2b9d9c1618f4a13", size = 164803, upload-time = "2024-11-26T14:33:26.272Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/65/6d3004b011a799781f379bdba531a3c19bc5f8cd929bafa96fc066a59b12/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3453cd138a43cdcce32cbbbdc99d99472fb7905e56df8ff2f73dac5be70f0657", size = 162855, upload-time = "2024-11-26T14:33:27.85Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/b8/35c8f8cedfeaf2ddb261f09e9a618a3e5ce44c8d4ea29e19ce4c1ae175d7/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a9acf658749ee324750643df040a62401d479de9a4507ca8f69bcf02df1b189", size = 176207, upload-time = "2024-11-26T14:33:29.062Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ea/428c9404f41bce80d946ed904b5749a5b3ac2f2a6a1a48f4da1c9b58f8b2/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ebfe6976a10ab6fb84d885622a39ff580803f3244a048b75fb63a97048cb894c", size = 167761, upload-time = "2024-11-26T14:33:31.473Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/97/b0e5fc755c78ac23a6e7a5c81288e7b44131becc11af07ef087f54054adf/libvalkey-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:860862322eceb3ed2ff2031663ee42d9ff4146226af3734f818b54a70339c440", size = 165463, upload-time = "2024-11-26T14:33:32.831Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/fd/9700a1edec4ebacbcb7ccdf55ac6548f2a5693b016768d721c0d520753ad/libvalkey-4.0.1-cp311-cp311-win32.whl", hash = "sha256:dd96985818cc1ddc8882dda67fa1cf711db37d0a24a4cd70897fd83a7377a11c", size = 19475, upload-time = "2024-11-26T14:33:33.986Z" },
+ { url = "https://files.pythonhosted.org/packages/85/25/d59dbdf8cb16d5c1f9215fc7cf66d2cbca6d05008eda1104b321df785647/libvalkey-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:4a1174d53d949f38128efc038b2d77cb79c4db127f5707ad350de0cda3aa9451", size = 21358, upload-time = "2024-11-26T14:33:35.487Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/62/fb85f94411890d233c74aad7b581bb65a0f809b5757446a280a8fa42a50d/libvalkey-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3d5da92598f8e6a82a5e3dedd49dae39fce6b3089030e17681e2b73df4a6d89", size = 43057, upload-time = "2024-11-26T14:33:36.532Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/cb/6f7614cab744f0e4e0eae583b2997bf22ffee4aa333e26786b91342986b6/libvalkey-4.0.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ce623bb8c37beb16d0f2c7c5b7080a3172dae4030e3bcd71326c7731883f766f", size = 82198, upload-time = "2024-11-26T14:33:38.044Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/de/9515ba0f436c3e54331b66cf7652c03fc73688e0b6f22853a6fc2cc8aa23/libvalkey-4.0.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:00adc978a791e944e2f6b442212cd3494a8d683ed215ff785dc9384968b212b6", size = 44839, upload-time = "2024-11-26T14:33:41.112Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f1/dd28a89f6c89e4fbcd95be18a04758f6f57fc69d38a7bc22a59377b277fc/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:827ea20b5ee1d99cf3d2c10da24c356c55836dd6ff1689b3fbf190b5bffe1605", size = 173314, upload-time = "2024-11-26T14:33:42.737Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/17/debc72596eb3e4c27a4ae1a5b5636e99b7b5e606c072c8816358ab69fb7f/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f81f7d806e5dd826532c0a4b6d8bc91a485fba65a3767cfdeb796b493ac59c8c", size = 167968, upload-time = "2024-11-26T14:33:44.196Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/00/451b234f5125e0b9396d7ca4b9d2ab9785e21475f4da60ff019e48912f73/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fe45323bbabee8d770127c0a763182a0d540a8c1afe6537d97dcc021fc26c4", size = 184371, upload-time = "2024-11-26T14:33:45.463Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c1/e10266e11034af9194feacec78221bb01db6961b662303a980c77a61f159/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c20ec7a26163dad23b0dfdbb81dd13ae3aa4083942b438c07dadaed88fa0ca6c", size = 173422, upload-time = "2024-11-26T14:33:47.165Z" },
+ { url = "https://files.pythonhosted.org/packages/88/ba/fe3b25281e41546ea96c41b7899d2a83a262463432280e07daed44beb7f5/libvalkey-4.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c0d5b4b01c2e4e5bad8339d8b585f42c0b10fb05a6e4469c15c43d925be6285", size = 173617, upload-time = "2024-11-26T14:33:48.403Z" },
+ { url = "https://files.pythonhosted.org/packages/05/b8/a75b6edcaabdc6245ee719357d307e2fccb375ca09d012327fbc1ef68771/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c151a43b250b6e6412c5b0a75093c49d248bbe541db91d2d7f04fd523ea616b3", size = 167110, upload-time = "2024-11-26T14:33:49.728Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/ae/602254b8865a0fb21df3f3cd57815ca7e6049cd79318bfb426449b661cee/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c8f59bb57f8e02e287da54a2e1250b9d591131922c006b873e5e2cad50fc36c", size = 164932, upload-time = "2024-11-26T14:33:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c6/116f5432c8234630079f3dadcf48828db41c2bfcdfaeb36ef197a2efa380/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:89e79fade6e6953c786b0e2d255a2930733f5d9e7ef07cf47a4eb0db4eabba5e", size = 178905, upload-time = "2024-11-26T14:33:52.416Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/00/ec095e022b7e5c2035787ad7389e0a11157ceb98f4ceae7fc44805907be0/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56495ab791f539dc3ee30378f9783f017e000ad8b03751ad2426003f74eee0bc", size = 170350, upload-time = "2024-11-26T14:33:53.769Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/84/d8bd771b07854c58cbf8926d98fed1701a4dcbe97ef31e8fea63416fc461/libvalkey-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:df9d7ba691c49c632bdc953b5c0af5c50231d0ae3bb0397688f63257a12786c0", size = 168125, upload-time = "2024-11-26T14:33:55.212Z" },
+ { url = "https://files.pythonhosted.org/packages/45/88/00d5d2d0960d0023c52d25d5ae87d47b5aec995241788be5c424826727aa/libvalkey-4.0.1-cp312-cp312-win32.whl", hash = "sha256:a39ad585b3d2d48d6f5b60941f9d6a5f3f30a396ae129db15bf626316f71594f", size = 19651, upload-time = "2024-11-26T14:33:56.625Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/57/a3f837524d63ed927f6ab3da3be2050f6c838279c796d5b44b617cad0047/libvalkey-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:7b39754c9cdf7fe704c636a2ea179c17229566e7c79af453df3a604b98879dc3", size = 21451, upload-time = "2024-11-26T14:33:57.755Z" },
+ { url = "https://files.pythonhosted.org/packages/51/3c/8571abc9b8a78281312b99348bccb35dfa161a3a3e9b963afdd473beea40/libvalkey-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f72346eca7408cfd6d6f407e7e548040b310ed6fdaec7d9ea67b49f20dc90a9e", size = 43065, upload-time = "2024-11-26T14:33:58.883Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/08/197217ff273fde5670b84682a2d67fe372890d14595ae0a15284626975ba/libvalkey-4.0.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:66b4558ca5b8fa48fde40dfc79547779d78c5397906f5ea5671b9a75f0952ff4", size = 82206, upload-time = "2024-11-26T14:34:00.181Z" },
+ { url = "https://files.pythonhosted.org/packages/51/e1/a806533c315cf758812e4f609548ccbb51c10221e2a3c6f9aa6e17633ee9/libvalkey-4.0.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:33d29f8d826b59e972a8502e8547d5fef7b1a1376fa0884cf1360c15977bca50", size = 44840, upload-time = "2024-11-26T14:34:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/c8/9908fdb4a5661bef8972cf94d149f5c5199c3835bba1a6f523225e2d6c37/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8adea3c5824937c1e94bb1d5bc30c57661e8bbcb1a79e8ead77b45bbe206f488", size = 173200, upload-time = "2024-11-26T14:34:02.453Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d2/3b6c235393137b16dacddb04ac6b632f3998cd10cb52b1e34b2eb7bdcabf/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50e956e1bc0ab21e2479fb5729456ae74de808a1be799b9163bf7a25029eeb41", size = 168270, upload-time = "2024-11-26T14:34:06.909Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b3/844123ae52d63d9ec97f4ad026054a0b616d83fd886adcfd2f8484939044/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56d6db545639a5fbb1c634621634a7f87845b7867056b1460a8299cc489e3364", size = 184290, upload-time = "2024-11-26T14:34:08.335Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/13/390d1f1fac2167e5e4531cbc090ec99e974271fc05e4c0c8597db593596d/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b83e133826ca50506c9f49b5c4fd64ef0dc3bbc6e85bb18b781a08320bf3f0db", size = 173279, upload-time = "2024-11-26T14:34:09.721Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/9f/74deb4ea77efb2cbf24c2a7364628b93f3bd02bff7203162c88e9bfe8f13/libvalkey-4.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e58b6dcea57df7ee8d80f914ed8895141fbb53d6f344b310ebe6cae3e407d0f", size = 173436, upload-time = "2024-11-26T14:34:11.091Z" },
+ { url = "https://files.pythonhosted.org/packages/db/e6/9bb87d6d2fe4ba4da960104356c2b165766cb1d420ef1d7f0f671729f3ab/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35576248ac379e755cf40c4ebe6bf735f278d46b5e449d0c8ea9f66869e3a8d4", size = 167050, upload-time = "2024-11-26T14:34:12.451Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/aa/35eda3faa3a7e9a5702c1833cf2ff0fdb024661d8342b24393198db968a2/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2be9cd8533638be94956567602554bbffa65d6fc8e758cd628f317a58cbcafda", size = 164934, upload-time = "2024-11-26T14:34:14.707Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/54/6439403407317d1228f8b2d5e38917ff162cd86b371bf28953e727674b20/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a03058988844b5a56f296d0d5608bbbe38df1b875246d6c6d7b442af793b5197", size = 178943, upload-time = "2024-11-26T14:34:16.31Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/54/8628c445f9683d2b0f2df3cf2084ec48917d33ccf3f857b2b4b25c6ba3a8/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:885a2ca644a2fdaf555e9fdab2bbe7de0f91de4e2a07726751efa35417736d55", size = 170398, upload-time = "2024-11-26T14:34:17.671Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/86/9780ad4d818fbb63efb3763041109fbdbe20a901413effa70a8fcf0ec56b/libvalkey-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:90fb5252391a8a9a3bb2a4eadd3f24a8af1032821d818e620da16854fb52503e", size = 168104, upload-time = "2024-11-26T14:34:19.625Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b2/8ec653c1e1cb85a8135f6413be90e40c1d3c6732f5933f4d3990f1777d09/libvalkey-4.0.1-cp313-cp313-win32.whl", hash = "sha256:ac6d515ece9c769ce8a0692fcb0d2ceeb5a71503a7db0cbfef0c255b5fb56b19", size = 19657, upload-time = "2024-11-26T14:34:20.909Z" },
+ { url = "https://files.pythonhosted.org/packages/61/92/f0b490a7bd7749aeed9e0dfca1f73e860a82ccb3ddb729591e4324c54367/libvalkey-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:125ff9fdba066a97e4bbdea69649184a5f5fcb832106bbaca81b80ff8dbee55c", size = 21461, upload-time = "2024-11-26T14:34:22.01Z" },
+ { url = "https://files.pythonhosted.org/packages/39/7d/8dec58f9f2f0f4eb8b1b861a4ee5ef2c8e7b63a46f0ffbba274f5170125b/libvalkey-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:75528a996263e57880d12a5b1421d90f3b63d8f65e1229af02075079ba17be0a", size = 37210, upload-time = "2024-11-26T14:35:06.987Z" },
+ { url = "https://files.pythonhosted.org/packages/77/d9/857376c3e0af4988d1b8ad13e8ee964dcfae9860e1917febd4f6b0819feb/libvalkey-4.0.1-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:4ee4f42a117cc172286f3180fdd2db9d74e7d3f28f97466e29d69e7c126eb084", size = 39638, upload-time = "2024-11-26T14:35:08.209Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/ae/9c0fcf578a56d860a9e056e67fbdff54b33952a88b63c384bc05b0cfe215/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb120643254a1b44fee2d07a9cb2eef80e65635ac6cd82af7c76b4c09941575e", size = 48760, upload-time = "2024-11-26T14:35:09.51Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/3d/76fa39c775c33e356be5b7b687b85d7fea512e1fd314a17b75f143e85b67/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e62c472774dd38d9e5809d4bea07ec6309a088e2102ddce8beef5a3e0d68b76", size = 56262, upload-time = "2024-11-26T14:35:12.351Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/cb/60131ef56e5152829c834c9c55cc5facb0a1988ba909c23d43f13bb65e0d/libvalkey-4.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a73ce687cda35c1a07c24bfb09b3b877c3d74837b59674cded032d03d12c1e55", size = 48920, upload-time = "2024-11-26T14:35:16.5Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ac/69d01a8e2ad5c94bd261784b8e8c2be3992b48492009baf56fcd16d1ab15/libvalkey-4.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1cd593e96281b80f5831292b9befe631a88415c68ad0748fe3d69101a093c501", size = 21366, upload-time = "2024-11-26T14:35:17.808Z" },
+]
+
[[package]]
name = "markdown-it-py"
version = "4.0.0"
@@ -3857,6 +3933,7 @@ source = { editable = "instrumentation/opentelemetry-instrumentation-redis" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-redis-valkey-base" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "wrapt" },
]
@@ -3870,12 +3947,32 @@ instruments = [
requires-dist = [
{ name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
{ name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-redis-valkey-base", editable = "instrumentation/opentelemetry-instrumentation-redis-valkey-base" },
{ name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" },
{ name = "redis", marker = "extra == 'instruments'", specifier = ">=2.6" },
{ name = "wrapt", specifier = ">=1.12.1" },
]
provides-extras = ["instruments"]
+[[package]]
+name = "opentelemetry-instrumentation-redis-valkey-base"
+source = { editable = "instrumentation/opentelemetry-instrumentation-redis-valkey-base" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "wrapt" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
+ { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" },
+ { name = "wrapt", specifier = ">=1.0.0,<2.0.0" },
+]
+provides-extras = ["instruments"]
+
[[package]]
name = "opentelemetry-instrumentation-remoulade"
source = { editable = "instrumentation/opentelemetry-instrumentation-remoulade" }
@@ -4132,6 +4229,33 @@ requires-dist = [
]
provides-extras = ["instruments"]
+[[package]]
+name = "opentelemetry-instrumentation-valkey"
+source = { editable = "instrumentation/opentelemetry-instrumentation-valkey" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-redis-valkey-base" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "wrapt" },
+]
+
+[package.optional-dependencies]
+instruments = [
+ { name = "valkey", extra = ["libvalkey"] },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" },
+ { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-redis-valkey-base", editable = "instrumentation/opentelemetry-instrumentation-redis-valkey-base" },
+ { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" },
+ { name = "valkey", extras = ["libvalkey"], marker = "extra == 'instruments'", specifier = ">=6.1.0" },
+ { name = "wrapt", specifier = ">=1.12.1" },
+]
+provides-extras = ["instruments"]
+
[[package]]
name = "opentelemetry-instrumentation-vertexai"
source = { editable = "instrumentation-genai/opentelemetry-instrumentation-vertexai" }
@@ -4288,6 +4412,7 @@ dependencies = [
{ name = "opentelemetry-instrumentation-tortoiseorm", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-urllib" },
{ name = "opentelemetry-instrumentation-urllib3", extra = ["instruments"] },
+ { name = "opentelemetry-instrumentation-valkey", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-vertexai", extra = ["instruments"] },
{ name = "opentelemetry-instrumentation-wsgi" },
{ name = "opentelemetry-propagator-aws-xray" },
@@ -4362,6 +4487,7 @@ requires-dist = [
{ name = "opentelemetry-instrumentation-tortoiseorm", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-tortoiseorm" },
{ name = "opentelemetry-instrumentation-urllib", editable = "instrumentation/opentelemetry-instrumentation-urllib" },
{ name = "opentelemetry-instrumentation-urllib3", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-urllib3" },
+ { name = "opentelemetry-instrumentation-valkey", extras = ["instruments"], editable = "instrumentation/opentelemetry-instrumentation-valkey" },
{ name = "opentelemetry-instrumentation-vertexai", extras = ["instruments"], editable = "instrumentation-genai/opentelemetry-instrumentation-vertexai" },
{ name = "opentelemetry-instrumentation-wsgi", editable = "instrumentation/opentelemetry-instrumentation-wsgi" },
{ name = "opentelemetry-propagator-aws-xray", editable = "propagator/opentelemetry-propagator-aws-xray" },
@@ -6198,6 +6324,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" },
]
+[[package]]
+name = "valkey"
+version = "6.1.1"
+source = { registry = "https://pypi.org/simple/" }
+dependencies = [
+ { name = "async-timeout", marker = "python_full_version < '3.11.3'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/ee/7fd930fc712275084722ddd464a0ea296abdb997d2da396320507968daeb/valkey-6.1.1.tar.gz", hash = "sha256:5880792990c6c2b5eb604a5ed5f98f300880b6dd92d123819b66ed54bb259731", size = 4601372, upload-time = "2025-08-11T06:41:10.63Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/a2/252afa4da08c714460f49e943070f86a02931f99f886182765194002fe33/valkey-6.1.1-py3-none-any.whl", hash = "sha256:e2691541c6e1503b53c714ad9a35551ac9b7c0bbac93865f063dbc859a46de92", size = 259474, upload-time = "2025-08-11T06:41:08.769Z" },
+]
+
+[package.optional-dependencies]
+libvalkey = [
+ { name = "libvalkey" },
+]
+
[[package]]
name = "venusian"
version = "3.1.1"