open-telemetry / opentelemetry-python-contrib

OpenTelemetry instrumentation for Python modules
https://opentelemetry.io
Apache License 2.0
648 stars 535 forks source link

Redis instrumentation doesn't work properly with Redis Cluster #2505

Open Grey-Fox opened 1 month ago

Grey-Fox commented 1 month ago

Spans from redis instrumentation don't contain a lot of attributes, such as "db.system", "net.transport" and so on. It happens because RedisCluster instance doesn't have connection_pool attribute.

Describe your environment Ubuntu 22.04 Python 3.10.13

Packages:

asgiref==3.8.1
async-timeout==4.0.3
Deprecated==1.2.14
importlib-metadata==7.0.0
opentelemetry-api==1.24.0
opentelemetry-distro==0.45b0
opentelemetry-instrumentation==0.45b0
opentelemetry-instrumentation-asgi==0.45b0
opentelemetry-instrumentation-asyncio==0.45b0
opentelemetry-instrumentation-aws-lambda==0.45b0
opentelemetry-instrumentation-dbapi==0.45b0
opentelemetry-instrumentation-logging==0.45b0
opentelemetry-instrumentation-redis==0.45b0
opentelemetry-instrumentation-sqlite3==0.45b0
opentelemetry-instrumentation-urllib==0.45b0
opentelemetry-instrumentation-wsgi==0.45b0
opentelemetry-propagator-aws-xray==1.0.1
opentelemetry-sdk==1.24.0
opentelemetry-semantic-conventions==0.45b0
opentelemetry-test-utils==0.45b0
opentelemetry-util-http==0.45b0
redis==5.0.4
typing_extensions==4.11.0
wrapt==1.16.0
zipp==3.18.1

Steps to reproduce main.py file:

from redis import RedisCluster

if __name__ == '__main__':
    redis = RedisCluster(host='localhost', port=6380)
    redis.set('key', 'value')
    print(redis.get('key'))

docker-compose.yaml for Redis Cluster:

services:
  redis-node-0:
    image: bitnami/redis-cluster:latest
    network_mode: host
    environment:
      - 'ALLOW_EMPTY_PASSWORD=yes'
      - 'REDIS_PORT_NUMBER=6380'
      - 'REDIS_NODES=localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384,localhost:6385'

  redis-node-1:
    image: bitnami/redis-cluster:latest
    network_mode: host
    environment:
      - 'ALLOW_EMPTY_PASSWORD=yes'
      - 'REDIS_PORT_NUMBER=6381'
      - 'REDIS_NODES=localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384,localhost:6385'

  redis-node-2:
    image: bitnami/redis-cluster:latest
    network_mode: host
    environment:
      - 'ALLOW_EMPTY_PASSWORD=yes'
      - 'REDIS_PORT_NUMBER=6382'
      - 'REDIS_NODES=localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384,localhost:6385

  redis-node-3:
    image: bitnami/redis-cluster:latest
    network_mode: host
    environment:
      - 'ALLOW_EMPTY_PASSWORD=yes'
      - 'REDIS_PORT_NUMBER=6383'
      - 'REDIS_NODES=localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384,localhost:6385'

  redis-node-4:
    image: bitnami/redis-cluster:latest
    network_mode: host
    environment:
      - 'ALLOW_EMPTY_PASSWORD=yes'
      - 'REDIS_PORT_NUMBER=6384'
      - 'REDIS_NODES=localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384,localhost:6385'

  redis-node-5:
    image: bitnami/redis-cluster:latest
    network_mode: host
    depends_on:
      - redis-node-0
      - redis-node-1
      - redis-node-2
      - redis-node-3
      - redis-node-4
    environment:
      - 'ALLOW_EMPTY_PASSWORD=yes'
      - 'REDIS_PORT_NUMBER=6385'
      - 'REDIS_NODES=localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384,localhost:6385'
      - 'REDIS_CLUSTER_REPLICAS=1'
      - 'REDIS_CLUSTER_CREATOR=yes'

Run Redis Cluster:

docker compose up

Run main.py:

opentelemetry-instrument \
    --traces_exporter console \
    --metrics_exporter console \
    --logs_exporter console \
    --service_name test \
    python main.py

What is the expected behavior? There are "db.system" and other attributes in span.

What is the actual behavior?

{
    "name": "GET",
    "context": {
        "trace_id": "0x992cb2db5d1253c9c9491669f5329079",
        "span_id": "0x9dffc53ee2108907",
        "trace_state": "[]"
    },
    "kind": "SpanKind.CLIENT",
    "parent_id": null,
    "start_time": "2024-05-07T14:58:25.048832Z",
    "end_time": "2024-05-07T14:58:25.049004Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "db.statement": "GET ?",
        "db.redis.args_length": 2
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.24.0",
            "service.name": "test",
            "telemetry.auto.version": "0.45b0"
        },
        "schema_url": ""
    }
}
a-cid commented 1 week ago

Here's a workaround:

from opentelemetry.instrumentation.redis import (
    RedisInstrumentor,
    _set_connection_attributes,
)

def _redis_request_hook(span, conn, args, kwargs):
    if hasattr(conn, "connection_pool") or not span.is_recording():
        return

    if hasattr(conn, "nodes_manager"):  # means we're working with RedisCluster
        _set_connection_attributes(
            span, conn.nodes_manager.default_node.redis_connection
        )

RedisInstrumentor().instrument(request_hook=_redis_request_hook)

edit: actually you should use the response_hook. Otherwise commands that use a ClusterPipeline (like set_many) will still have missing attributes since the ClusterPipeline.execute_command wrapper doesn't call request_hook.