Skip to content

Commit f1a33dd

Browse files
authored
Added intrastructure and integration point with OTel (#3864)
* Added intrastructure and integration point with OTel * Added check for enabled metric groups * Applied comments
1 parent 742b13b commit f1a33dd

File tree

12 files changed

+3613
-0
lines changed

12 files changed

+3613
-0
lines changed

dev_requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@ numpy>=1.24.0,<2.0 ; platform_python_implementation == "PyPy"
3030

3131
redis-entraid==1.0.0
3232
pybreaker>=1.4.0
33+
34+
opentelemetry-api>=1.18.0
35+
opentelemetry-sdk>=1.18.0
36+
opentelemetry-exporter-otlp-http>=1.18.0

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ jwt = [
4545
circuit_breaker = [
4646
"pybreaker>=1.4.0"
4747
]
48+
otel = [
49+
"opentelemetry-api>=1.18.0",
50+
"opentelemetry-sdk>=1.18.0",
51+
"opentelemetry-exporter-otlp-http>=1.18.0",
52+
]
4853

4954
[project.urls]
5055
Changes = "https://github.com/redis/redis-py/releases"

redis/observability/__init__.py

Whitespace-only changes.

redis/observability/attributes.py

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
"""
2+
OpenTelemetry semantic convention attributes for Redis.
3+
4+
This module provides constants and helper functions for building OTel attributes
5+
according to the semantic conventions for database clients.
6+
7+
Reference: https://opentelemetry.io/docs/specs/semconv/database/redis/
8+
"""
9+
from enum import Enum
10+
from typing import Any, Dict, Optional
11+
12+
# Database semantic convention attributes
13+
DB_SYSTEM = "db.system"
14+
DB_NAMESPACE = "db.namespace"
15+
DB_OPERATION_NAME = "db.operation.name"
16+
DB_OPERATION_BATCH_SIZE = "db.operation.batch.size"
17+
DB_RESPONSE_STATUS_CODE = "db.response.status_code"
18+
DB_STORED_PROCEDURE_NAME = "db.stored_procedure.name"
19+
20+
# Error attributes
21+
ERROR_TYPE = "error.type"
22+
23+
# Network attributes
24+
NETWORK_PEER_ADDRESS = "network.peer.address"
25+
NETWORK_PEER_PORT = "network.peer.port"
26+
27+
# Server attributes
28+
SERVER_ADDRESS = "server.address"
29+
SERVER_PORT = "server.port"
30+
31+
# Connection pool attributes
32+
DB_CLIENT_CONNECTION_POOL_NAME = "db.client.connection.pool.name"
33+
DB_CLIENT_CONNECTION_STATE = "db.client.connection.state"
34+
35+
# Redis-specific attributes
36+
REDIS_CLIENT_LIBRARY = "redis.client.library"
37+
REDIS_CLIENT_CONNECTION_PUBSUB = "redis.client.connection.pubsub"
38+
REDIS_CLIENT_CONNECTION_CLOSE_REASON = "redis.client.connection.close.reason"
39+
REDIS_CLIENT_CONNECTION_NOTIFICATION = "redis.client.connection.notification"
40+
REDIS_CLIENT_OPERATION_RETRY_ATTEMPTS = "redis.client.operation.retry_attempts"
41+
REDIS_CLIENT_OPERATION_BLOCKING = "redis.client.operation.blocking"
42+
REDIS_CLIENT_PUBSUB_MESSAGE_DIRECTION = "redis.client.pubsub.message.direction"
43+
REDIS_CLIENT_PUBSUB_CHANNEL = "redis.client.pubsub.channel"
44+
REDIS_CLIENT_PUBSUB_SHARDED = "redis.client.pubsub.sharded"
45+
REDIS_CLIENT_ERROR_INTERNAL = "redis.client.errors.internal"
46+
REDIS_CLIENT_STREAM_NAME = "redis.client.stream.name"
47+
REDIS_CLIENT_CONSUMER_GROUP = "redis.client.consumer_group"
48+
REDIS_CLIENT_CONSUMER_NAME = "redis.client.consumer_name"
49+
50+
class ConnectionState(Enum):
51+
IDLE = "idle"
52+
USED = "used"
53+
54+
class PubSubDirection(Enum):
55+
PUBLISH = "publish"
56+
RECEIVE = "receive"
57+
58+
59+
class AttributeBuilder:
60+
"""
61+
Helper class to build OTel semantic convention attributes for Redis operations.
62+
"""
63+
64+
@staticmethod
65+
def build_base_attributes(
66+
server_address: Optional[str] = None,
67+
server_port: Optional[int] = None,
68+
db_namespace: Optional[int] = None,
69+
) -> Dict[str, Any]:
70+
"""
71+
Build base attributes common to all Redis operations.
72+
73+
Args:
74+
server_address: Redis server address (FQDN or IP)
75+
server_port: Redis server port
76+
db_namespace: Redis database index
77+
78+
Returns:
79+
Dictionary of base attributes
80+
"""
81+
attrs: Dict[str, Any] = {
82+
DB_SYSTEM: "redis",
83+
REDIS_CLIENT_LIBRARY: "redis-py"
84+
}
85+
86+
if server_address is not None:
87+
attrs[SERVER_ADDRESS] = server_address
88+
89+
if server_port is not None:
90+
attrs[SERVER_PORT] = server_port
91+
92+
if db_namespace is not None:
93+
attrs[DB_NAMESPACE] = str(db_namespace)
94+
95+
return attrs
96+
97+
@staticmethod
98+
def build_operation_attributes(
99+
command_name: Optional[str] = None,
100+
batch_size: Optional[int] = None,
101+
response_status_code: Optional[str] = None,
102+
error_type: Optional[Exception] = None,
103+
network_peer_address: Optional[str] = None,
104+
network_peer_port: Optional[int] = None,
105+
stored_procedure_name: Optional[str] = None,
106+
retry_attempts: Optional[int] = None,
107+
is_blocking: Optional[bool] = None,
108+
) -> Dict[str, Any]:
109+
"""
110+
Build attributes for a Redis operation (command execution).
111+
112+
Args:
113+
command_name: Redis command name (e.g., 'GET', 'SET', 'MULTI')
114+
batch_size: Number of commands in batch (for pipelines/transactions)
115+
response_status_code: Redis error prefix (e.g., 'ERR', 'WRONGTYPE')
116+
error_type: Error type if operation failed
117+
network_peer_address: Resolved peer address
118+
network_peer_port: Peer port number
119+
stored_procedure_name: Lua script name or SHA1 digest
120+
retry_attempts: Number of retry attempts made
121+
is_blocking: Whether the operation is a blocking command
122+
123+
Returns:
124+
Dictionary of operation attributes
125+
"""
126+
attrs: Dict[str, Any] = {}
127+
128+
if command_name is not None:
129+
attrs[DB_OPERATION_NAME] = command_name.upper()
130+
131+
if batch_size is not None and batch_size >= 2:
132+
attrs[DB_OPERATION_BATCH_SIZE] = batch_size
133+
134+
if response_status_code is not None:
135+
attrs[DB_RESPONSE_STATUS_CODE] = response_status_code
136+
137+
if error_type is not None:
138+
attrs[ERROR_TYPE] = AttributeBuilder.extract_error_type(error_type)
139+
140+
if network_peer_address is not None:
141+
attrs[NETWORK_PEER_ADDRESS] = network_peer_address
142+
143+
if network_peer_port is not None:
144+
attrs[NETWORK_PEER_PORT] = network_peer_port
145+
146+
if stored_procedure_name is not None:
147+
attrs[DB_STORED_PROCEDURE_NAME] = stored_procedure_name
148+
149+
if retry_attempts is not None and retry_attempts > 0:
150+
attrs[REDIS_CLIENT_OPERATION_RETRY_ATTEMPTS] = retry_attempts
151+
152+
if is_blocking is not None:
153+
attrs[REDIS_CLIENT_OPERATION_BLOCKING] = is_blocking
154+
155+
return attrs
156+
157+
@staticmethod
158+
def build_connection_attributes(
159+
pool_name: str,
160+
connection_state: Optional[ConnectionState] = None,
161+
is_pubsub: Optional[bool] = None,
162+
) -> Dict[str, Any]:
163+
"""
164+
Build attributes for connection pool metrics.
165+
166+
Args:
167+
pool_name: Unique connection pool name
168+
connection_state: Connection state ('idle' or 'used')
169+
is_pubsub: Whether this is a PubSub connection
170+
171+
Returns:
172+
Dictionary of connection pool attributes
173+
"""
174+
attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes()
175+
attrs[DB_CLIENT_CONNECTION_POOL_NAME] = pool_name
176+
177+
if connection_state is not None:
178+
attrs[DB_CLIENT_CONNECTION_STATE] = connection_state.value
179+
180+
if is_pubsub is not None:
181+
attrs[REDIS_CLIENT_CONNECTION_PUBSUB] = is_pubsub
182+
183+
return attrs
184+
185+
@staticmethod
186+
def build_error_attributes(
187+
is_internal: bool = False,
188+
error_type: Optional[Exception] = None,
189+
) -> Dict[str, Any]:
190+
"""
191+
Build error attributes.
192+
193+
Args:
194+
is_internal: Whether the error is internal (e.g., timeout, network error)
195+
error_type: The exception that occurred
196+
197+
Returns:
198+
Dictionary of error attributes
199+
"""
200+
attrs: Dict[str, Any] = {REDIS_CLIENT_ERROR_INTERNAL: is_internal}
201+
202+
if error_type is not None:
203+
attrs[DB_RESPONSE_STATUS_CODE] = None
204+
205+
return attrs
206+
207+
@staticmethod
208+
def build_pubsub_message_attributes(
209+
direction: PubSubDirection,
210+
channel: Optional[str] = None,
211+
sharded: Optional[bool] = None,
212+
) -> Dict[str, Any]:
213+
"""
214+
Build attributes for a PubSub message.
215+
216+
Args:
217+
direction: Message direction ('publish' or 'receive')
218+
channel: Pub/Sub channel name
219+
sharded: True if sharded Pub/Sub channel
220+
221+
Returns:
222+
Dictionary of PubSub message attributes
223+
"""
224+
attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes()
225+
attrs[REDIS_CLIENT_PUBSUB_MESSAGE_DIRECTION] = direction.value
226+
227+
if channel is not None:
228+
attrs[REDIS_CLIENT_PUBSUB_CHANNEL] = channel
229+
230+
if sharded is not None:
231+
attrs[REDIS_CLIENT_PUBSUB_SHARDED] = sharded
232+
233+
return attrs
234+
235+
@staticmethod
236+
def build_streaming_attributes(
237+
stream_name: Optional[str] = None,
238+
consumer_group: Optional[str] = None,
239+
consumer_name: Optional[str] = None,
240+
) -> Dict[str, Any]:
241+
"""
242+
Build attributes for a streaming operation.
243+
244+
Args:
245+
stream_name: Name of the stream
246+
consumer_group: Name of the consumer group
247+
consumer_name: Name of the consumer
248+
249+
Returns:
250+
Dictionary of streaming attributes
251+
"""
252+
attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes()
253+
254+
if stream_name is not None:
255+
attrs[REDIS_CLIENT_STREAM_NAME] = stream_name
256+
257+
if consumer_group is not None:
258+
attrs[REDIS_CLIENT_CONSUMER_GROUP] = consumer_group
259+
260+
if consumer_name is not None:
261+
attrs[REDIS_CLIENT_CONSUMER_NAME] = consumer_name
262+
263+
return attrs
264+
265+
266+
@staticmethod
267+
def extract_error_type(exception: Exception) -> str:
268+
"""
269+
Extract error type from an exception.
270+
271+
Args:
272+
exception: The exception that occurred
273+
274+
Returns:
275+
Error type string (exception class name)
276+
"""
277+
return type(exception).__name__
278+
279+
@staticmethod
280+
def build_pool_name(
281+
server_address: str,
282+
server_port: int,
283+
db_namespace: int = 0,
284+
) -> str:
285+
"""
286+
Build a unique connection pool name.
287+
288+
Args:
289+
server_address: Redis server address
290+
server_port: Redis server port
291+
db_namespace: Redis database index
292+
293+
Returns:
294+
Unique pool name in format "address:port/db"
295+
"""
296+
return f"{server_address}:{server_port}/{db_namespace}"

0 commit comments

Comments
 (0)