diff --git a/redis_client.py b/redis_client.py index 0a2a514..0bedd5d 100644 --- a/redis_client.py +++ b/redis_client.py @@ -1,6 +1,7 @@ import socket -class RedisClient(): + +class RedisClient: def __init__(self, host, port, auth): self.host = host self.port = port @@ -10,10 +11,10 @@ def connect(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((self.host, self.port)) - self.file = self.socket.makefile('r') + self.file = self.socket.makefile("rb") if self.auth is not None: - self.send('auth %s' % (self.auth)) + self.send("auth %s" % (self.auth)) self.read_response() @@ -26,21 +27,21 @@ def __exit__(self, *args): self.socket.close() def send(self, message): - return self.socket.sendall("%s\r\n" % message) + return self.socket.sendall(message.encode("utf-8") + b"\r\n") def read_response(self): - first_line = self.file.readline() - if first_line.startswith('-'): + first_line = self.file.readline().decode("utf-8") + if first_line.startswith("-"): raise RedisError(first_line) - if first_line.startswith('*'): + if first_line.startswith("*"): return self.read_array(first_line) - elif first_line.startswith('$'): + elif first_line.startswith("$"): return self.read_bulk_string(first_line) - elif first_line.startswith(':'): - return first_line.lstrip(':').rstrip() - elif first_line.startswith('+'): - return first_line.lstrip('+').rstrip() + elif first_line.startswith(":"): + return first_line.lstrip(":").rstrip() + elif first_line.startswith("+"): + return first_line.lstrip("+").rstrip() else: raise ValueError("Unknown Redis response: %s" % first_line) @@ -53,7 +54,7 @@ def read_bulk_string(self, first_line): if size == -1: return None - s = self.file.read(size) + s = self.file.read(size).decode("utf-8") # Get rid of \r\n at end self.file.read(2) diff --git a/redis_info.py b/redis_info.py index d41876c..565954c 100644 --- a/redis_info.py +++ b/redis_info.py @@ -29,13 +29,14 @@ # collectd-python: # http://collectd.org/documentation/manpages/collectd-python.5.shtml -import collectd -import socket import re +import socket + +import collectd from redis_client import RedisClient, RedisError -class RedisCollector(): +class RedisCollector: def __init__(self, host, port, auth, instance, metric_types, verbose, llen_keys): self.host = host self.port = port @@ -47,18 +48,17 @@ def __init__(self, host, port, auth, instance, metric_types, verbose, llen_keys) def fetch_info(self, client): """Request info from the Redis server""" - self.log_verbose('Sending info command') - client.send('info') + self.log_verbose("Sending info command") + client.send("info") try: data = client.read_response() - except RedisError, e: - collectd.error('redis_info plugin: Error response from %s:%d - %r' - % (self.host, self.port, e)) + except RedisError as e: + collectd.error("redis_info plugin: Error response from %s:%d - %r" % (self.host, self.port, e)) return None - self.log_verbose('Received data: %s' % data) + self.log_verbose("Received data: %s" % data) - linesep = '\r\n' if '\r\n' in data else '\n' + linesep = "\r\n" if "\r\n" in data else "\n" info_dict = self.parse_info(data.split(linesep)) return info_dict @@ -67,34 +67,31 @@ def parse_info(self, info_lines): """Parse info response from Redis""" info = {} for line in info_lines: - if "" == line or line.startswith('#'): + if not line or line.startswith("#"): continue - if ':' not in line: - collectd.warning('redis_info plugin: Bad format for info line: %s' - % line) + if ":" not in line: + collectd.warning("redis_info plugin: Bad format for info line: %s" % line) continue - key, val = line.split(':') + key, val = line.split(":") # Handle multi-value keys (for dbs and slaves). # db lines look like "db0:keys=10,expire=0" # slave lines look like # "slave0:ip=192.168.0.181,port=6379, # state=online,offset=1650991674247,lag=1" - if ',' in val: - split_val = val.split(',') + if "," in val: + split_val = val.split(",") for sub_val in split_val: - k, _, v = sub_val.rpartition('=') + k, _, v = sub_val.rpartition("=") sub_key = "{0}_{1}".format(key, k) info[sub_key] = v else: info[key] = val # compatibility with pre-2.6 redis (used changes_since_last_save) - info["changes_since_last_save"] = info.get("changes_since_last_save", - info.get( - "rdb_changes_since_last_save")) + info["changes_since_last_save"] = info.get("changes_since_last_save", info.get("rdb_changes_since_last_save")) return info @@ -102,23 +99,19 @@ def dispatch_info(self, client): info = self.fetch_info(client) if not info: - collectd.error('redis plugin: No info received') + collectd.error("redis plugin: No info received") return - for keyTuple, val in self.metric_types.iteritems(): + for keyTuple, val in self.metric_types.items(): key, mtype = keyTuple - if key == 'total_connections_received' and mtype == 'counter': - self.dispatch_value(info['total_connections_received'], - 'counter', - type_instance='connections_received') - elif key == 'total_commands_processed' and mtype == 'counter': - self.dispatch_value(info['total_commands_processed'], - 'counter', - type_instance='commands_processed') + if key == "total_connections_received" and mtype == "counter": + self.dispatch_value(info["total_connections_received"], "counter", type_instance="connections_received") + elif key == "total_commands_processed" and mtype == "counter": + self.dispatch_value(info["total_commands_processed"], "counter", type_instance="commands_processed") else: try: self.dispatch_value(info[key], mtype, type_instance=key) - except KeyError: + except (KeyError, TypeError): collectd.error("Metric %s not found in Redis INFO output" % key) continue @@ -129,19 +122,19 @@ def dispatch_list_lengths(self, client): database index. """ key_dict = {} - for db, patterns in self.llen_keys.items(): - client.send('select %d' % db) + for db, patterns in list(self.llen_keys.items()): + client.send("select %d" % db) try: resp = client.read_response() - except RedisError, e: + except RedisError as e: collectd.error("Could not select Redis db %s: %s" % (db, e)) continue for pattern in patterns: keys = [] # If there is a glob, get every key matching it - if '*' in pattern: - client.send('KEYS %s' % pattern) + if "*" in pattern: + client.send("KEYS %s" % pattern) keys = client.read_response() else: keys = [pattern] @@ -150,18 +143,17 @@ def dispatch_list_lengths(self, client): self.fetch_and_dispatch_llen_for_key(client, key, db) def fetch_and_dispatch_llen_for_key(self, client, key, db): - client.send('llen %s' % key) + client.send("llen %s" % key) try: val = client.read_response() - except RedisError, e: - collectd.warning('redis_info plugin: could not get length of key %s in db %s: %s' % (key, db, e)) + except RedisError as e: + collectd.warning("redis_info plugin: could not get length of key %s in db %s: %s" % (key, db, e)) return - dimensions = {'key_name': key, 'db_index': db} - self.dispatch_value(val, 'gauge', type_instance='key_llen', dimensions=dimensions) + dimensions = {"key_name": key, "db_index": db} + self.dispatch_value(val, "gauge", type_instance="key_llen", dimensions=dimensions) - def dispatch_value(self, value, type, plugin_instance=None, type_instance=None, - dimensions={}): + def dispatch_value(self, value, type, plugin_instance=None, type_instance=None, dimensions={}): """Read a key from info response data and dispatch a value""" try: @@ -169,17 +161,16 @@ def dispatch_value(self, value, type, plugin_instance=None, type_instance=None, except ValueError: value = float(value) - self.log_verbose('Sending value: %s=%s (%s)' % (type_instance, value, dimensions)) + self.log_verbose("Sending value: %s=%s (%s)" % (type_instance, value, dimensions)) - val = collectd.Values(plugin='redis_info') + val = collectd.Values(plugin="redis_info") val.type = type val.type_instance = type_instance val.values = [value] plugin_instance = self.instance if plugin_instance is None: - plugin_instance = '{host}:{port}'.format(host=self.host, - port=self.port) + plugin_instance = "{host}:{port}".format(host=self.host, port=self.port) val.plugin_instance = "{0}{1}".format(plugin_instance, _format_dimensions(dimensions)) @@ -187,24 +178,22 @@ def dispatch_value(self, value, type, plugin_instance=None, type_instance=None, # to each value for it to be correctly serialized to JSON by the # write_http plugin. See # https://github.com/collectd/collectd/issues/716 - val.meta = {'0': True} + val.meta = {"0": True} val.dispatch() def read_callback(self): try: self.get_metrics() - except socket.error, e: - collectd.error('redis_info plugin: Error connecting to %s:%d - %r' - % (self.host, self.port, e)) + except socket.error as e: + collectd.error("redis_info plugin: Error connecting to %s:%d - %r" % (self.host, self.port, e)) return - except RedisError, e: - collectd.error('redis_info plugin: Error getting metrics from %s:%d - %r' - % (self.host, self.port, e)) + except RedisError as e: + collectd.error("redis_info plugin: Error getting metrics from %s:%d - %r" % (self.host, self.port, e)) return def get_metrics(self): with RedisClient(self.host, self.port, self.auth) as client: - self.log_verbose('Connected to Redis at %s:%s' % (self.host, self.port)) + self.log_verbose("Connected to Redis at %s:%s" % (self.host, self.port)) self.dispatch_info(client) self.dispatch_list_lengths(client) @@ -212,8 +201,7 @@ def get_metrics(self): def log_verbose(self, msg): if not self.verbose: return - collectd.info('redis plugin [verbose]: %s' % msg) - + collectd.info("redis plugin [verbose]: %s" % msg) def configure_callback(conf): @@ -229,44 +217,50 @@ def configure_callback(conf): for node in conf.children: key = node.key.lower() val = node.values[0] - searchObj = re.search(r'redis_(.*)$', key, re.M | re.I) + searchObj = re.search(r"redis_(.*)$", key, re.M | re.I) - if key == 'host': + if key == "host": host = val - elif key == 'port': + elif key == "port": port = int(val) - elif key == 'auth': + elif key == "auth": auth = val - elif key == 'verbose': + elif key == "verbose": verbose = node.values[0] - elif key == 'instance': + elif key == "instance": instance = val - elif key == 'sendlistlength': + elif key == "sendlistlength": if (len(node.values)) == 2: llen_keys.setdefault(int(node.values[0]), []).append(node.values[1]) else: - collectd.warning("redis_info plugin: monitoring length of keys requires both \ - database index and key value") + collectd.warning( + "redis_info plugin: monitoring length of keys requires both \ + database index and key value" + ) elif searchObj: metric_types[searchObj.group(1), val] = True else: - collectd.warning('redis_info plugin: Unknown config key: %s.' % - key) + collectd.warning("redis_info plugin: Unknown config key: %s." % key) continue if verbose: - collectd.info('Configured with host=%s, port=%s, instance name=%s, using_auth=%s, llen_keys=%s' - % (host, port, instance, (auth is not None), llen_keys)) - - collector = RedisCollector(**{ - 'host': host, - 'port': port, - 'auth': auth, - 'instance': instance, - 'metric_types': metric_types, - 'verbose': verbose, - 'llen_keys': llen_keys}) + collectd.info( + "Configured with host=%s, port=%s, instance name=%s, using_auth=%s, llen_keys=%s" + % (host, port, instance, (auth is not None), llen_keys) + ) + + collector = RedisCollector( + **{ + "host": host, + "port": port, + "auth": auth, + "instance": instance, + "metric_types": metric_types, + "verbose": verbose, + "llen_keys": llen_keys, + } + ) collectd.register_read(collector.read_callback, name="%s:%s:%s" % (host, port, instance)) @@ -286,10 +280,9 @@ def _format_dimensions(dimensions): if not dimensions: return "" - dim_pairs = ["%s=%s" % (k, v) for k, v in dimensions.iteritems()] + dim_pairs = ["%s=%s" % (k, v) for k, v in dimensions.items()] return "[%s]" % (",".join(dim_pairs)) - # register callbacks collectd.register_config(configure_callback)