How to inspect Redis internals using UNIX socket and Python script

Inspect Redis internals using UNIX socket and Python script without external dependencies.

Look at the end of this blog post for additional information including protocol specification.

Python script

This is a simplest possible implementation without external dependencies.

#!/usr/bin/env python3
# Source: 
#   https://blog.sleeplessbeastie.eu/2019/05/01/how-to-inspect-redis-internals-using-unix-socket-and-python-script/

import sys
import socket

class RedisCLI:
    # Indicate request size
    REQUEST_SIZE = '*'

    # Indicate element size
    REQUEST_ELEMENT_SIZE = '$'

    # Indicate the terminator
    REQUEST_TERMINATOR = '\r\n'

    # Socket buffer size
    SOCKET_BUFFER_SIZE = 8

    def __init__(self, socket_path="/var/run/redis/redis.sock", socket_timeout=5.0, password=''):
        self.socket_path = socket_path
        self.socket_timeout = socket_timeout
        self.password = password

    def set_socket_timeout(self):
        self.socket.settimeout(self.socket_timeout)

    def connect(self):
        try:
            self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            self.set_socket_timeout()
            self.socket.connect(self.socket_path)
        except:
            print(sys.exc_info()[1])
            sys.exit(1)

    def read_raw(self):
        reply = []
        while True:
            reply_part = self.socket.recv(self.SOCKET_BUFFER_SIZE)
            reply.append(reply_part.decode())
            if len(reply_part) < self.SOCKET_BUFFER_SIZE:
                break
        return "".join(reply)

    def read(self):
        return self.resp_parse(self.read_raw())

    def resp_parse(self, reply):
        first_line = reply.splitlines()[0]
        data_type = first_line[0]
        data = first_line[1:]

        if data_type == '-':
            response = ' '.join(('(error)', data))
        elif data_type == '+':
            response = ''
        elif data_type == ':':
            response = int(data)
        elif data_type == "$":
            response = '\r\n'.join(reply.split('\r\n')[1:])[:int(data)]
        elif data_type == '*':
            response = [self.resp_parse(''.join(('$', '\r\n'.join(reply.split('\r\n')[1:]).split('$')[i]))) for i in range(1, int(data) + 1)]
        else:
            response = '(error) parse error'
        return response

    def send(self, command):
        self.socket.send(self.resp_create(command))

    def close(self):
        self.socket.close()

    def resp_create(self, command):
        elements = command.split(" ")
        resp = ''.join((self.REQUEST_SIZE, str(len(elements)), self.REQUEST_TERMINATOR))
        for element in elements:
            resp = ''.join((resp, self.REQUEST_ELEMENT_SIZE, str(len(element)), self.REQUEST_TERMINATOR, element,
                            self.REQUEST_TERMINATOR))
        return resp.encode()

    def execute(self, command):
        try:
            if self.password:
                self.send(' '.join(('AUTH', self.password)))
                reply = self.read_raw()
                if reply[0] != '+':
                    raise Exception(reply)

            self.send(command)
            reply = self.read()
            if isinstance(reply, str):
                return '\n'.join(reply.splitlines())
            else:
                return reply

        except Exception:
            print(sys.exc_info()[1])
            sys.exit(2)

    def request(self, command):
        self.connect()
        data = self.execute(command)
        self.close()
        return data


redis_connection = RedisCLI(socket_path="/var/run/redis/redis-local.sock", password='x2G7xn-eR13rrLT')

print("---- CPU usage")
print(redis_connection.request("INFO CPU"))

print()
print("---- Client list")
print(redis_connection.request("CLIENT LIST"))

print()
print("---- BIND config setting")
print(redis_connection.request("CONFIG GET bind"))

print()
print("---- Available commands")
print(redis_connection.request("COMMAND"))

Sample usage

This script returns some sample internals like CPU usage, client list, BIND configuration setting and available commands. You can easily extend it to analyze and report data using desired format.

---- CPU usage
# CPU
used_cpu_sys:64.80
used_cpu_user:22.84
used_cpu_sys_children:0.00
used_cpu_user_children:0.00

---- Client list
id=82 addr=127.0.0.1:42714 fd=9 name= age=90793 idle=90793 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=command
id=568 addr=/var/run/redis/redis-local.sock:0 fd=10 name= age=82717 idle=82717 flags=U db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=info
id=1573 addr=/var/run/redis/redis-local.sock:0 fd=11 name= age=0 idle=0 flags=U db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

---- BIND config setting
['bind', '127.0.0.1 ::1']

---- Available commands
['bitpos', 'post', 'zrange', 'lset', 'incrby', 'unlink', 'swapdb', 'sunionstore', 'debug', 'sync', 'scard', 'georadiusbymember_ro', 'cluster', 'zscore', 'hvals', 'sdiff', 'geopos', 'blpop', 'sunion', 'msetnx', 'substr', 'bgrewriteaof', 'save', 'lpushx', 'eval', 'flushdb', 'sort', 'brpop', 'zscan', 'hset', 'append', 'rpop', 'sadd', 'renamenx', 'georadius', 'hexists', 'bgsave', 'psync', 'auth', 'expireat', 'bitcount', 'sdiffstore', 'migrate', 'setnx', 'touch', 'script', 'zrevrangebyscore', 'set', 'hincrbyfloat', 'shutdown', 'publish', 'mset', 'type', 'lrange', 'latency', 'hget', 'select', 'zrem', 'del', 'incrbyfloat', 'geoadd', 'bitfield', 'psetex', 'getbit', 'dbsize', 'host:', 'incr', 'srandmember', 'sinterstore', 'watch', 'punsubscribe', 'zrevrangebylex', 'zrevrange', 'exists', 'randomkey', 'role', 'zunionstore', 'unsubscribe', 'replconf', 'pubsub', 'monitor', 'scan', 'lpop', 'psubscribe', 'pttl', 'dump', 'zrangebylex', 'zremrangebyrank', 'hdel', 'hstrlen', 'asking', 'lindex', 'hmget', 'zrevrank', 'expire', 'rename', 'zrank', 'zcard', 'setbit', 'pfadd', 'hlen', 'keys', 'geohash', 'discard', 'client', 'hincrby', 'spop', 'pfcount', 'georadius_ro', 'get', 'linsert', 'hscan', 'pfselftest', 'command', 'ltrim', 'ping', 'geodist', 'zremrangebyscore', 'pexpire', 'flushall', 'llen', 'memory', 'module', 'unwatch', 'slaveof', 'move', 'subscribe', 'hgetall', 'smembers', 'lastsave', 'pfdebug', 'zincrby', 'exec', 'decrby', 'sismember', 'zinterstore', 'config', 'pexpireat', 'pfmerge', 'mget', 'rpush', 'multi', 'slowlog', 'rpushx', 'hsetnx', 'time', 'readwrite', 'readonly', 'zlexcount', 'decr', 'sinter', 'georadiusbymember', 'object', 'zremrangebylex', 'hkeys', 'restore', 'sscan', 'brpoplpush', 'srem', 'ttl', 'echo', 'setex', 'lrem', 'evalsha', 'restore-asking', 'zrangebyscore', 'strlen', 'getset', 'zadd', 'bitop', 'lpush', 'rpoplpush', 'info', 'setrange', 'smove', 'getrange', 'zcount', 'persist', 'hmset', 'wait']

Additional information

It was a very fun experience! Source code for Redis Python Client helped me a lot.

Milosz Galazka's Picture

About Milosz Galazka

Milosz is a Linux Foundation Certified Engineer working for a successful Polish company as a system administrator and a long time supporter of Free Software Foundation and Debian operating system.