""" OpenTelemetry semantic convention attributes for Redis. This module provides constants and helper functions for building OTel attributes according to the semantic conventions for database clients. Reference: https://opentelemetry.io/docs/specs/semconv/database/redis/ """ from enum import Enum from typing import TYPE_CHECKING, Any, Dict, Optional, Union import redis if TYPE_CHECKING: from redis.asyncio.multidb.database import AsyncDatabase from redis.connection import ConnectionPoolInterface from redis.multidb.database import SyncDatabase # Database semantic convention attributes DB_SYSTEM = "db.system" DB_NAMESPACE = "db.namespace" DB_OPERATION_NAME = "db.operation.name" DB_RESPONSE_STATUS_CODE = "db.response.status_code" DB_STORED_PROCEDURE_NAME = "db.stored_procedure.name" # Error attributes ERROR_TYPE = "error.type" # Network attributes NETWORK_PEER_ADDRESS = "network.peer.address" NETWORK_PEER_PORT = "network.peer.port" # Server attributes SERVER_ADDRESS = "server.address" SERVER_PORT = "server.port" # Connection pool attributes DB_CLIENT_CONNECTION_POOL_NAME = "db.client.connection.pool.name" DB_CLIENT_CONNECTION_STATE = "db.client.connection.state" DB_CLIENT_CONNECTION_NAME = "db.client.connection.name" # Geofailover attributes DB_CLIENT_GEOFAILOVER_FAIL_FROM = "db.client.geofailover.fail_from" DB_CLIENT_GEOFAILOVER_FAIL_TO = "db.client.geofailover.fail_to" DB_CLIENT_GEOFAILOVER_REASON = "db.client.geofailover.reason" # Redis-specific attributes REDIS_CLIENT_LIBRARY = "redis.client.library" REDIS_CLIENT_CONNECTION_PUBSUB = "redis.client.connection.pubsub" REDIS_CLIENT_CONNECTION_CLOSE_REASON = "redis.client.connection.close.reason" REDIS_CLIENT_CONNECTION_NOTIFICATION = "redis.client.connection.notification" REDIS_CLIENT_OPERATION_RETRY_ATTEMPTS = "redis.client.operation.retry_attempts" REDIS_CLIENT_OPERATION_BLOCKING = "redis.client.operation.blocking" REDIS_CLIENT_PUBSUB_MESSAGE_DIRECTION = "redis.client.pubsub.message.direction" REDIS_CLIENT_PUBSUB_CHANNEL = "redis.client.pubsub.channel" REDIS_CLIENT_PUBSUB_SHARDED = "redis.client.pubsub.sharded" REDIS_CLIENT_ERROR_INTERNAL = "redis.client.errors.internal" REDIS_CLIENT_ERROR_CATEGORY = "redis.client.errors.category" REDIS_CLIENT_STREAM_NAME = "redis.client.stream.name" REDIS_CLIENT_CONSUMER_GROUP = "redis.client.consumer_group" REDIS_CLIENT_CSC_RESULT = "redis.client.csc.result" REDIS_CLIENT_CSC_REASON = "redis.client.csc.reason" class ConnectionState(Enum): IDLE = "idle" USED = "used" class PubSubDirection(Enum): PUBLISH = "publish" RECEIVE = "receive" class CSCResult(Enum): HIT = "hit" MISS = "miss" class CSCReason(Enum): FULL = "full" INVALIDATION = "invalidation" class GeoFailoverReason(Enum): AUTOMATIC = "automatic" MANUAL = "manual" class AttributeBuilder: """ Helper class to build OTel semantic convention attributes for Redis operations. """ @staticmethod def build_base_attributes( server_address: Optional[str] = None, server_port: Optional[int] = None, db_namespace: Optional[int] = None, ) -> Dict[str, Any]: """ Build base attributes common to all Redis operations. Args: server_address: Redis server address (FQDN or IP) server_port: Redis server port db_namespace: Redis database index Returns: Dictionary of base attributes """ attrs: Dict[str, Any] = { DB_SYSTEM: "redis", REDIS_CLIENT_LIBRARY: f"redis-py:v{redis.__version__}", } if server_address is not None: attrs[SERVER_ADDRESS] = server_address if server_port is not None: attrs[SERVER_PORT] = server_port if db_namespace is not None: attrs[DB_NAMESPACE] = str(db_namespace) return attrs @staticmethod def build_operation_attributes( command_name: Optional[str] = None, batch_size: Optional[int] = None, # noqa network_peer_address: Optional[str] = None, network_peer_port: Optional[int] = None, stored_procedure_name: Optional[str] = None, retry_attempts: Optional[int] = None, is_blocking: Optional[bool] = None, ) -> Dict[str, Any]: """ Build attributes for a Redis operation (command execution). Args: command_name: Redis command name (e.g., 'GET', 'SET', 'MULTI') batch_size: Number of commands in batch (for pipelines/transactions) network_peer_address: Resolved peer address network_peer_port: Peer port number stored_procedure_name: Lua script name or SHA1 digest retry_attempts: Number of retry attempts made is_blocking: Whether the operation is a blocking command Returns: Dictionary of operation attributes """ attrs: Dict[str, Any] = {} if command_name is not None: attrs[DB_OPERATION_NAME] = command_name.upper() if network_peer_address is not None: attrs[NETWORK_PEER_ADDRESS] = network_peer_address if network_peer_port is not None: attrs[NETWORK_PEER_PORT] = network_peer_port if stored_procedure_name is not None: attrs[DB_STORED_PROCEDURE_NAME] = stored_procedure_name if retry_attempts is not None and retry_attempts > 0: attrs[REDIS_CLIENT_OPERATION_RETRY_ATTEMPTS] = retry_attempts if is_blocking is not None: attrs[REDIS_CLIENT_OPERATION_BLOCKING] = is_blocking return attrs @staticmethod def build_connection_attributes( pool_name: Optional[str] = None, connection_state: Optional[ConnectionState] = None, connection_name: Optional[str] = None, is_pubsub: Optional[bool] = None, ) -> Dict[str, Any]: """ Build attributes for connection pool metrics. Args: pool_name: Unique connection pool name connection_state: Connection state ('idle' or 'used') is_pubsub: Whether this is a PubSub connection connection_name: Unique connection name Returns: Dictionary of connection pool attributes """ attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes() if pool_name is not None: attrs[DB_CLIENT_CONNECTION_POOL_NAME] = pool_name if connection_state is not None: attrs[DB_CLIENT_CONNECTION_STATE] = connection_state.value if is_pubsub is not None: attrs[REDIS_CLIENT_CONNECTION_PUBSUB] = is_pubsub if connection_name is not None: attrs[DB_CLIENT_CONNECTION_NAME] = connection_name return attrs @staticmethod def build_error_attributes( error_type: Optional[Exception] = None, is_internal: Optional[bool] = None, ) -> Dict[str, Any]: """ Build error attributes. Args: is_internal: Whether the error is internal (e.g., timeout, network error) error_type: The exception that occurred Returns: Dictionary of error attributes """ attrs: Dict[str, Any] = {} if error_type is not None: attrs[ERROR_TYPE] = error_type.__class__.__name__ if ( hasattr(error_type, "status_code") and error_type.status_code is not None ): attrs[DB_RESPONSE_STATUS_CODE] = error_type.status_code else: attrs[DB_RESPONSE_STATUS_CODE] = "error" if hasattr(error_type, "error_type") and error_type.error_type is not None: attrs[REDIS_CLIENT_ERROR_CATEGORY] = error_type.error_type.value else: attrs[REDIS_CLIENT_ERROR_CATEGORY] = "other" if is_internal is not None: attrs[REDIS_CLIENT_ERROR_INTERNAL] = is_internal return attrs @staticmethod def build_pubsub_message_attributes( direction: PubSubDirection, channel: Optional[str] = None, sharded: Optional[bool] = None, ) -> Dict[str, Any]: """ Build attributes for a PubSub message. Args: direction: Message direction ('publish' or 'receive') channel: Pub/Sub channel name sharded: True if sharded Pub/Sub channel Returns: Dictionary of PubSub message attributes """ attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes() attrs[REDIS_CLIENT_PUBSUB_MESSAGE_DIRECTION] = direction.value if channel is not None: attrs[REDIS_CLIENT_PUBSUB_CHANNEL] = channel if sharded is not None: attrs[REDIS_CLIENT_PUBSUB_SHARDED] = sharded return attrs @staticmethod def build_streaming_attributes( stream_name: Optional[str] = None, consumer_group: Optional[str] = None, consumer_name: Optional[str] = None, # noqa ) -> Dict[str, Any]: """ Build attributes for a streaming operation. Args: stream_name: Name of the stream consumer_group: Name of the consumer group consumer_name: Name of the consumer Returns: Dictionary of streaming attributes """ attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes() if stream_name is not None: attrs[REDIS_CLIENT_STREAM_NAME] = stream_name if consumer_group is not None: attrs[REDIS_CLIENT_CONSUMER_GROUP] = consumer_group return attrs @staticmethod def build_csc_attributes( pool_name: Optional[str] = None, result: Optional[CSCResult] = None, reason: Optional[CSCReason] = None, ) -> Dict[str, Any]: """ Build attributes for a Client Side Caching (CSC) operation. Args: pool_name: Connection pool name (used only for csc_items metric) result: CSC result ('hit' or 'miss') reason: Reason for CSC eviction ('full' or 'invalidation') Returns: Dictionary of CSC attributes """ attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes() if pool_name is not None: attrs[DB_CLIENT_CONNECTION_POOL_NAME] = pool_name if result is not None: attrs[REDIS_CLIENT_CSC_RESULT] = result.value if reason is not None: attrs[REDIS_CLIENT_CSC_REASON] = reason.value return attrs @staticmethod def build_geo_failover_attributes( fail_from: Union["SyncDatabase", "AsyncDatabase"], fail_to: Union["SyncDatabase", "AsyncDatabase"], reason: GeoFailoverReason, ) -> Dict[str, Any]: """ Build attributes for a geo failover. Args: fail_from: Database failed from fail_to: Database failed to reason: Reason for the failover Returns: Dictionary of geo failover attributes """ attrs: Dict[str, Any] = AttributeBuilder.build_base_attributes() attrs[DB_CLIENT_GEOFAILOVER_FAIL_FROM] = get_db_name(fail_from) attrs[DB_CLIENT_GEOFAILOVER_FAIL_TO] = get_db_name(fail_to) attrs[DB_CLIENT_GEOFAILOVER_REASON] = reason.value return attrs @staticmethod def build_pool_name( server_address: str, server_port: int, db_namespace: int = 0, ) -> str: """ Build a unique connection pool name. Args: server_address: Redis server address server_port: Redis server port db_namespace: Redis database index Returns: Unique pool name in format "address:port/db" """ return f"{server_address}:{server_port}/{db_namespace}" def get_pool_name(pool: "ConnectionPoolInterface") -> str: """ Get a short string representation of a connection pool for observability. This provides a concise pool identifier suitable for use as a metric attribute, in the format: host:port_uniqueID (matching go-redis format) Args: pool: Connection pool instance Returns: Short pool name in format "host:port_uniqueID" Example: >>> pool = ConnectionPool(host='localhost', port=6379, db=0) >>> get_pool_name(pool) 'localhost:6379_a1b2c3d4' """ host = pool.connection_kwargs.get("host", "unknown") port = pool.connection_kwargs.get("port", 6379) # Get unique pool ID if available (added for observability) pool_id = getattr(pool, "_pool_id", "") if pool_id: return f"{host}:{port}_{pool_id}" else: return f"{host}:{port}" def get_db_name(database: Union["SyncDatabase", "AsyncDatabase"]): """ Get a short string representation of a database for observability. Args: database: Database instance Returns: Short database name in format "{host}:{port}/{weight}" """ host = database.client.get_connection_kwargs()["host"] port = database.client.get_connection_kwargs()["port"] weight = database.weight return f"{host}:{port}/{weight}"