1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
"""Memcache metrics.""" |
3
|
|
|
import sys |
4
|
|
|
import time |
5
|
|
|
import socket |
6
|
|
|
import os.path |
7
|
|
|
|
8
|
|
|
import plumd |
9
|
|
|
from plumd.util import Differential |
10
|
|
|
|
11
|
|
|
PY3 = sys.version_info > (3,) |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
__author__ = 'Kenny Freeman' |
15
|
|
|
__email__ = '[email protected]' |
16
|
|
|
__license__ = "ISCL" |
17
|
|
|
__docformat__ = 'reStructuredText' |
18
|
|
|
|
19
|
|
|
|
20
|
|
|
class Memcache(plumd.Reader): |
21
|
|
|
"""Plugin to record memcache stats.""" |
22
|
|
|
|
23
|
|
|
STAT_CMD = "stats\n" |
24
|
|
|
RECV_SIZE = 4096 |
25
|
|
|
|
26
|
|
|
# default config values |
27
|
|
|
defaults = { |
28
|
|
|
'poll.interval': 10, |
29
|
|
|
'gauges': [ |
30
|
|
|
"auth_cmds", |
31
|
|
|
"auth_errors", |
32
|
|
|
"bytes", |
33
|
|
|
"bytes_read", |
34
|
|
|
"bytes_written", |
35
|
|
|
"cas_badval", |
36
|
|
|
"cas_hits", |
37
|
|
|
"cas_misses", |
38
|
|
|
"cmd_flush", |
39
|
|
|
"cmd_get", |
40
|
|
|
"cmd_set", |
41
|
|
|
"cmd_touch", |
42
|
|
|
"connection_structures", |
43
|
|
|
"conn_yields", |
44
|
|
|
"curr_connections", |
45
|
|
|
"curr_items", |
46
|
|
|
"decr_hits", |
47
|
|
|
"decr_misses", |
48
|
|
|
"delete_hits", |
49
|
|
|
"delete_misses", |
50
|
|
|
"evicted_unfetched", |
51
|
|
|
"evictions", |
52
|
|
|
"expired_unfetched", |
53
|
|
|
"get_hits", |
54
|
|
|
"get_misses", |
55
|
|
|
"hash_bytes", |
56
|
|
|
"hash_is_expanding", |
57
|
|
|
"hash_power_level", |
58
|
|
|
"incr_hits", |
59
|
|
|
"incr_misses", |
60
|
|
|
"limit_maxbytes", |
61
|
|
|
"listen_disabled_num", |
62
|
|
|
"reclaimed", |
63
|
|
|
"reserved_fds", |
64
|
|
|
"rusage_system", |
65
|
|
|
"rusage_user", |
66
|
|
|
"threads", |
67
|
|
|
"total_connections", |
68
|
|
|
"total_items", |
69
|
|
|
"touch_hits", |
70
|
|
|
"touch_misses", |
71
|
|
|
"uptime" |
72
|
|
|
], |
73
|
|
|
'rates': [], |
74
|
|
|
'host': '127.0.0.1', # memcache server hostname/ip |
75
|
|
|
'port': 11211, # memcache server port |
76
|
|
|
'socket': None, # use tcp or unix domain socket |
77
|
|
|
'timeout': 10 # connection timeouts |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
def __init__(self, log, config): |
81
|
|
|
"""Plugin to record memcache stats. |
82
|
|
|
|
83
|
|
|
:param log: A logger |
84
|
|
|
:type log: logging.RootLogger |
85
|
|
|
:param config: a plumd.config.Conf configuration helper instance. |
86
|
|
|
:type config: plumd.config.Conf |
87
|
|
|
""" |
88
|
|
|
super(Memcache, self).__init__(log, config) |
89
|
|
|
self.config.defaults(Memcache.defaults) |
90
|
|
|
|
91
|
|
|
# metrics to record |
92
|
|
|
self.gauges = self.config.get('gauges') |
93
|
|
|
self.rates = self.config.get('rates') |
94
|
|
|
|
95
|
|
|
# memcached connection - either unix socket or tcp |
96
|
|
|
spath = self.config.get('socket') |
97
|
|
|
if spath is None or not os.path.exists(spath): |
98
|
|
|
host = self.config.get('host') |
99
|
|
|
port = self.config.get('port') |
100
|
|
|
self.addr = (host, port) |
101
|
|
|
self.sfamily = socket.AF_INET |
102
|
|
|
else: |
103
|
|
|
self.addr = spath |
104
|
|
|
self.sfamily = socket.AF_UNIX |
105
|
|
|
|
106
|
|
|
self.socket = None |
107
|
|
|
self.timeout = config.get('timeout') |
108
|
|
|
self.calc = Differential() |
109
|
|
|
|
110
|
|
|
def poll(self): |
111
|
|
|
"""Query memcache for stats. |
112
|
|
|
|
113
|
|
|
:rtype: ResultSet |
114
|
|
|
""" |
115
|
|
|
result = plumd.Result("memcache") |
116
|
|
|
|
117
|
|
|
name = self.name |
118
|
|
|
stats = self.get_stats() |
119
|
|
|
times = time.time() |
120
|
|
|
|
121
|
|
|
# record gauges |
122
|
|
|
for stat in self.gauges: |
123
|
|
|
if stat in stats: |
124
|
|
|
mname = "{0}.{1}".format(name, stat) |
125
|
|
|
result.add(plumd.Float(mname, stats[stat])) |
126
|
|
|
|
127
|
|
|
# record rates |
128
|
|
|
for stat in self.rates: |
129
|
|
|
if stat in stats: |
130
|
|
|
mname = "{0}.{1}".format(name, stat) |
131
|
|
|
mval = self.calc.per_second(mname, float(stats[stat]), times) |
132
|
|
|
result.add(plumd.Float(mname, mval)) |
133
|
|
|
|
134
|
|
|
return plumd.ResultSet([result]) |
135
|
|
|
|
136
|
|
|
def get_stats(self): |
137
|
|
|
"""Request and read stats from memcache socket.""" |
138
|
|
|
stats = {} |
139
|
|
|
|
140
|
|
|
if not self.socket and not self.connect(): |
141
|
|
|
return {} |
142
|
|
|
|
143
|
|
|
try: |
144
|
|
|
if PY3: |
145
|
|
|
self.socket.sendall(bytes(Memcache.STAT_CMD, 'utf8')) |
146
|
|
|
else: |
147
|
|
|
self.socket.sendall(Memcache.STAT_CMD) |
148
|
|
|
|
149
|
|
|
st_str = self.socket.recv(Memcache.RECV_SIZE) |
150
|
|
|
|
151
|
|
|
for line in st_str.split("\n"): |
152
|
|
|
vals = line.split() |
153
|
|
|
if len(vals) != 3: |
154
|
|
|
continue |
155
|
|
|
stype, sname, sval = vals |
156
|
|
|
if stype == "STAT": |
157
|
|
|
msg = "Memcached: {0} = {1}" |
158
|
|
|
stats[sname] = sval |
159
|
|
|
|
160
|
|
|
except Exception as exc: |
|
|
|
|
161
|
|
|
msg = "Memcached: {0}: poll: exception: {1}" |
162
|
|
|
self.log.error(msg.format(self.addr, exc)) |
163
|
|
|
self.disconnect() |
164
|
|
|
|
165
|
|
|
return stats |
166
|
|
|
|
167
|
|
|
def connect(self): |
168
|
|
|
"""Connect to memcached, returns True if sucessful, False otherwise. |
169
|
|
|
|
170
|
|
|
:rtype: bool |
171
|
|
|
""" |
172
|
|
|
self.log.debug("Memcached: connecting: {0}".format(self.addr)) |
173
|
|
|
if self.socket: |
174
|
|
|
self.disconnect() |
175
|
|
|
try: |
176
|
|
|
# create the socket |
177
|
|
|
self.socket = socket.socket(self.sfamily, |
178
|
|
|
socket.SOCK_STREAM) |
179
|
|
|
# set timeout for socket operations |
180
|
|
|
self.socket.settimeout(self.timeout) |
181
|
|
|
# connect |
182
|
|
|
self.socket.connect(self.addr) |
183
|
|
|
# log and return |
184
|
|
|
msg = "Memcached: connected: {0}" |
185
|
|
|
self.log.info(msg.format(self.addr)) |
186
|
|
|
return True |
187
|
|
|
except Exception as exc: |
|
|
|
|
188
|
|
|
# log exception, ensure cleanup is done (disconnect) |
189
|
|
|
msg = "Memcached: {0}: connect: excception: {1}" |
190
|
|
|
self.log.error(msg.format(self.addr, exc)) |
191
|
|
|
return False |
192
|
|
|
return False |
193
|
|
|
|
194
|
|
|
def disconnect(self): |
195
|
|
|
"""Severe the memcached connection.""" |
196
|
|
|
if self.socket: |
197
|
|
|
try: |
198
|
|
|
self.socket.close() |
199
|
|
|
self.socket = None |
200
|
|
|
except Exception as exc: |
|
|
|
|
201
|
|
|
msg = "Memcached: dicsonnect: exception {0}".format(exc) |
202
|
|
|
self.log.error(msg) |
203
|
|
|
self.socket = None |
204
|
|
|
|
Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.
So, unless you specifically plan to handle any error, consider adding a more specific exception.