-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathdiamond_redis.py
325 lines (260 loc) · 10.2 KB
/
diamond_redis.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# coding=utf-8
"""
Collects data from one or more Redis Servers
#### Dependencies
* redis
#### Notes
The collector is named an odd redisstat because of an import issue with
having the python library called redis and this collector's module being called
redis, so we use an odd name for this collector. This doesn't affect the usage
of this collector.
Example config file RedisCollector.conf
```
enabled=True
host=redis.example.com
port=16379
auth=PASSWORD
```
or for multi-instance mode:
```
enabled=True
instances = nick1@host1:port1, nick2@host2:port2/PASSWORD, ...
```
For connecting via unix sockets, provide the path prefixed with ``unix:``
instead of the host, e.g.
```
enabled=True
host=unix:/var/run/redis/redis.sock
```
or
```
enabled = True
instances = nick3@unix:/var/run/redis.sock:/PASSWORD
```
In that case, for disambiguation there must be a colon ``:`` before the slash
``/`` followed by the password.
Note: when using the host/port config mode, the port number is used in
the metric key. When using the multi-instance mode, the nick will be used.
If not specified the port will be used. In case of unix sockets, the base name
without file extension (i.e. in the aforementioned examples ``redis``)
is the default metric key.
"""
import diamond.collector
import time
import os
import redis
SOCKET_PREFIX = 'unix:'
SOCKET_PREFIX_LEN = len(SOCKET_PREFIX)
class RedisCollector(diamond.collector.Collector):
_DATABASE_COUNT = 16
_DEFAULT_DB = 0
_DEFAULT_HOST = 'localhost'
_DEFAULT_PORT = 6379
_DEFAULT_SOCK_TIMEOUT = 5
_KEYS = {'clients.blocked': 'blocked_clients',
'clients.connected': 'connected_clients',
'clients.longest_output_list': 'client_longest_output_list',
'cpu.parent.sys': 'used_cpu_sys',
'cpu.children.sys': 'used_cpu_sys_children',
'cpu.parent.user': 'used_cpu_user',
'cpu.children.user': 'used_cpu_user_children',
'hash_max_zipmap.entries': 'hash_max_zipmap_entries',
'hash_max_zipmap.value': 'hash_max_zipmap_value',
'keys.evicted': 'evicted_keys',
'keys.expired': 'expired_keys',
'keyspace.hits': 'keyspace_hits',
'keyspace.misses': 'keyspace_misses',
'last_save.changes_since': 'changes_since_last_save',
'last_save.time': 'last_save_time',
'memory.internal_view': 'used_memory',
'memory.external_view': 'used_memory_rss',
'memory.fragmentation_ratio': 'mem_fragmentation_ratio',
'process.commands_processed': 'total_commands_processed',
'process.connections_received': 'total_connections_received',
'process.uptime': 'uptime_in_seconds',
'pubsub.channels': 'pubsub_channels',
'pubsub.patterns': 'pubsub_patterns',
'slaves.connected': 'connected_slaves',
'slaves.last_io': 'master_last_io_seconds_ago'}
_RENAMED_KEYS = {'last_save.changes_since': 'rdb_changes_since_last_save',
'last_save.time': 'rdb_last_save_time'}
def process_config(self):
super(RedisCollector, self).process_config()
instance_list = self.config['instances']
# configobj make str of single-element list, let's convert
if isinstance(instance_list, basestring):
instance_list = [instance_list]
# process original single redis instance
if len(instance_list) == 0:
host = self.config['host']
port = int(self.config['port'])
auth = self.config['auth']
if auth is not None:
instance_list.append('%s:%d/%s' % (host, port, auth))
else:
instance_list.append('%s:%d' % (host, port))
self.instances = {}
for instance in instance_list:
if '@' in instance:
(nickname, hostport) = instance.split('@', 1)
else:
nickname = None
hostport = instance
if hostport.startswith(SOCKET_PREFIX):
unix_socket, __, port_auth = hostport[
SOCKET_PREFIX_LEN:].partition(':')
auth = port_auth.partition('/')[2] or None
if nickname is None:
nickname = os.path.splitext(
os.path.basename(unix_socket))[0]
self.instances[nickname] = (self._DEFAULT_HOST,
self._DEFAULT_PORT,
unix_socket,
auth)
else:
if '/' in hostport:
parts = hostport.split('/')
hostport = parts[0]
auth = parts[1]
else:
auth = None
if ':' in hostport:
if hostport[0] == ':':
host = self._DEFAULT_HOST
port = int(hostport[1:])
else:
parts = hostport.split(':')
host = parts[0]
port = int(parts[1])
else:
host = hostport
port = self._DEFAULT_PORT
if nickname is None:
nickname = str(port)
self.instances[nickname] = (host, port, None, auth)
self.log.debug("Configured instances: %s" % self.instances.items())
def get_default_config_help(self):
config_help = super(RedisCollector, self).get_default_config_help()
config_help.update({
'host': 'Hostname to collect from',
'port': 'Port number to collect from',
'timeout': 'Socket timeout',
'db': '',
'auth': 'Password?',
'databases': 'how many database instances to collect',
'instances': "Redis addresses, comma separated, syntax:" +
" nick1@host:port, nick2@:port or nick3@host"
})
return config_help
def get_default_config(self):
"""
Return default config
:rtype: dict
"""
config = super(RedisCollector, self).get_default_config()
config.update({
'host': self._DEFAULT_HOST,
'port': self._DEFAULT_PORT,
'timeout': self._DEFAULT_SOCK_TIMEOUT,
'db': self._DEFAULT_DB,
'auth': None,
'databases': self._DATABASE_COUNT,
'path': 'redis',
'instances': [],
})
return config
def _client(self, host, port, unix_socket, auth):
"""Return a redis client for the configuration.
:param str host: redis host
:param int port: redis port
:rtype: redis.Redis
"""
db = int(self.config['db'])
timeout = int(self.config['timeout'])
try:
cli = redis.Redis(host=host, port=port,
db=db, socket_timeout=timeout, password=auth,
unix_socket_path=unix_socket)
cli.ping()
return cli
except Exception, ex:
self.log.error("RedisCollector: failed to connect to %s:%i. %s.",
unix_socket or host, port, ex)
def _precision(self, value):
"""Return the precision of the number
:param str value: The value to find the precision of
:rtype: int
"""
value = str(value)
decimal = value.rfind('.')
if decimal == -1:
return 0
return len(value) - decimal - 1
def _publish_key(self, nick, key):
"""Return the full key for the partial key.
:param str nick: Nickname for Redis instance
:param str key: The key name
:rtype: str
"""
return '%s.%s' % (nick, key)
def _get_info(self, host, port, unix_socket, auth):
"""Return info dict from specified Redis instance
:param str host: redis host
:param int port: redis port
:rtype: dict
"""
client = self._client(host, port, unix_socket, auth)
if client is None:
return None
info = client.info()
del client
return info
def collect_instance(self, nick, host, port, unix_socket, auth):
"""Collect metrics from a single Redis instance
:param str nick: nickname of redis instance
:param str host: redis host
:param int port: redis port
:param str unix_socket: unix socket, if applicable
:param str auth: authentication password
"""
# Connect to redis and get the info
info = self._get_info(host, port, unix_socket, auth)
if info is None:
return
# The structure should include the port for multiple instances per
# server
data = dict()
# Iterate over the top level keys
for key in self._KEYS:
if self._KEYS[key] in info:
data[key] = info[self._KEYS[key]]
# Iterate over renamed keys for 2.6 support
for key in self._RENAMED_KEYS:
if self._RENAMED_KEYS[key] in info:
data[key] = info[self._RENAMED_KEYS[key]]
# Look for databaase speific stats
for dbnum in range(0, int(self.config.get('databases',
self._DATABASE_COUNT))):
db = 'db%i' % dbnum
if db in info:
for key in info[db]:
data['%s.%s' % (db, key)] = info[db][key]
# Time since last save
for key in ['last_save_time', 'rdb_last_save_time']:
if key in info:
data['last_save.time_since'] = int(time.time()) - info[key]
# Publish the data to graphite
for key in data:
self.publish(self._publish_key(nick, key),
data[key],
precision=self._precision(data[key]),
metric_type='GAUGE')
def collect(self):
"""Collect the stats from the redis instance and publish them.
"""
if redis is None:
self.log.error('Unable to import module redis')
return {}
for nick in self.instances.keys():
(host, port, unix_socket, auth) = self.instances[nick]
self.collect_instance(nick, host, int(port), unix_socket, auth)