1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
|
3
|
|
|
__author__ = 'Kenny Freeman' |
4
|
|
|
__email__ = '[email protected]' |
5
|
|
|
__license__ = "ISCL" |
6
|
|
|
__docformat__ = 'reStructuredText' |
7
|
|
|
|
8
|
|
|
import sys |
9
|
|
|
|
10
|
|
|
PY3 = sys.version_info > (3,) |
11
|
|
|
|
12
|
|
|
import json |
13
|
|
|
import struct |
14
|
|
|
import socket |
15
|
|
|
from collections import deque |
16
|
|
|
|
17
|
|
|
import plumd |
18
|
|
|
import plumd.plugins |
19
|
|
|
|
20
|
|
|
|
21
|
|
|
class FcgiParameterProblem(Exception): |
22
|
|
|
"""Generic problem with FastCGI request data.""" |
23
|
|
|
pass |
24
|
|
|
|
25
|
|
|
class FcgiRequestFailed(Exception): |
26
|
|
|
"""Generic FastCGI request failed exception.""" |
27
|
|
|
pass |
28
|
|
|
|
29
|
|
|
# Number of bytes in a FCGI_Header |
30
|
|
|
FCGI_HEADER_LEN = 8 |
31
|
|
|
|
32
|
|
|
# Version of the FastCGI Protocol |
33
|
|
|
FCGI_VERSION_1 = 1 |
34
|
|
|
|
35
|
|
|
# Possible values for the type in FCGI_Header |
36
|
|
|
FCGI_BEGIN_REQUEST = 1 |
37
|
|
|
#FCGI_ABORT_REQUEST = 2 |
38
|
|
|
FCGI_END_REQUEST = 3 |
39
|
|
|
FCGI_PARAMS = 4 |
40
|
|
|
FCGI_STDIN = 5 |
41
|
|
|
FCGI_STDOUT = 6 |
42
|
|
|
FCGI_STDERR = 7 |
43
|
|
|
FCGI_DATA = 8 |
44
|
|
|
#FCGI_GET_VALUES = 9 |
45
|
|
|
#FCGI_GET_VALUES_RESULT = 10 |
46
|
|
|
FCGI_UNKNOWN_TYPE = 11 |
47
|
|
|
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE |
48
|
|
|
|
49
|
|
|
# Values for role in FCGI_BeginRequestBody |
50
|
|
|
FCGI_RESPONDER = 1 |
51
|
|
|
|
52
|
|
|
# Values for protocolStatus in FCGI_EndRequestBody |
53
|
|
|
FCGI_REQUEST_COMPLETE = 0 |
54
|
|
|
FCGI_CANT_MPX_CONN = 1 |
55
|
|
|
FCGI_OVERLOADED = 2 |
56
|
|
|
FCGI_UNKNOWN_ROLE = 3 |
57
|
|
|
|
58
|
|
|
# Numbe rof bytes in a FCGI_BeginRequestBody, etc |
59
|
|
|
FCGI_BEGIN_REQUEST_LEN = 8 |
60
|
|
|
|
61
|
|
|
|
62
|
|
|
def fcgi_header(dat): |
63
|
|
|
"""Return a dict with values unpacked from dat: |
64
|
|
|
|
65
|
|
|
dat = { |
66
|
|
|
'version': FCGI_VERSION_1, |
67
|
|
|
'type': FCGI_PARAMS, |
68
|
|
|
'id': 1, |
69
|
|
|
'data': 512, # data length |
70
|
|
|
'padding': 0 # padding length |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
:param dat: A packed FastCGI header string |
74
|
|
|
:type dat: str |
75
|
|
|
:rtype: dict |
76
|
|
|
""" |
77
|
|
|
keys = ['version', 'type', 'id', 'data_len', 'pad_len'] |
78
|
|
|
vals = struct.unpack("!BBHHBx", dat) |
79
|
|
|
return dict(zip(keys, vals)) |
80
|
|
|
|
81
|
|
|
def fcgi_end_request(dat): |
82
|
|
|
"""Return a tuple of (appstatus, protocolstatus) from the end request |
83
|
|
|
body dat. |
84
|
|
|
|
85
|
|
|
:param dat: A packed string representing the FCGI_EndRequestBody |
86
|
|
|
:type dat: str |
87
|
|
|
:rtype: tuple(int, int) |
88
|
|
|
""" |
89
|
|
|
astat, pstat = struct.unpack('!LB3x', dat) |
90
|
|
|
return (astat, pstat) |
91
|
|
|
|
92
|
|
|
def fcgi_unpack(buff): |
93
|
|
|
"""Return a dict containing all key, value pairs in the buffer. |
94
|
|
|
|
95
|
|
|
:param buff: str to unpack pairs from |
96
|
|
|
:type buff: str |
97
|
|
|
:rtype: dict |
98
|
|
|
""" |
99
|
|
|
ret = {} |
100
|
|
|
maxi = len(buff) |
101
|
|
|
i = 0 |
102
|
|
|
while i < maxi: |
103
|
|
|
# extract klen, vlen then key, val |
104
|
|
|
klen = ord(buff[i][0]) |
105
|
|
|
if klen < 128: |
106
|
|
|
i += 1 |
107
|
|
|
else: |
108
|
|
|
klen = struct.unpack("!L", struct_bytes)[0] & 0x7fffffff |
109
|
|
|
i += 4 |
110
|
|
|
# now get the val length |
111
|
|
|
vlen = ord(buff[i][0]) |
112
|
|
|
if vlen < 128: |
113
|
|
|
i += 1 |
114
|
|
|
else: |
115
|
|
|
vlen = struct.unpack("!L", struct_bytes)[0] & 0x7fffffff |
116
|
|
|
i += 4 |
117
|
|
|
# get the values |
118
|
|
|
key = buff[i:i+klen] |
119
|
|
|
i += klen |
120
|
|
|
val = buff[i:i+vlen] |
121
|
|
|
i += vlen |
122
|
|
|
ret[key] = val |
123
|
|
|
return ret |
124
|
|
|
|
125
|
|
|
def fcgi_pack(vals): |
126
|
|
|
"""Pack a dict of key=value pairs into a buffer. |
127
|
|
|
|
128
|
|
|
:param vals: A dict of key=value pairs |
129
|
|
|
:type vals: dict |
130
|
|
|
:rtype: str |
131
|
|
|
""" |
132
|
|
|
ret = deque() |
133
|
|
|
# pack: key_len+val_len+key+val |
134
|
|
|
for key, val in vals.items(): |
135
|
|
|
val = str(val) if val is not None else "" |
136
|
|
|
klen = len(key) |
137
|
|
|
vlen = len(val) |
138
|
|
|
|
139
|
|
|
# does key_len fit in a char? |
140
|
|
|
if klen < 128: |
141
|
|
|
ret.append(chr(klen)) |
142
|
|
|
else: |
143
|
|
|
# should check if len(key) > 0x7fffffff.. |
144
|
|
|
#long value: 0x80000000L = 2147483648 |
145
|
|
|
ret.append(struct.pack('!L', klen | 0x80000000)) |
146
|
|
|
|
147
|
|
|
# same, does it fit in a single char? |
148
|
|
|
if vlen < 128: |
149
|
|
|
ret.append(chr(vlen)) |
150
|
|
|
else: |
151
|
|
|
# should check if len(val) > 0x7fffffff.. |
152
|
|
|
#long value: 0x80000000L = 2147483648 |
153
|
|
|
ret.append(struct.pack('!L', vlen | 0x80000000)) |
154
|
|
|
ret.append(str(key)) |
155
|
|
|
ret.append(str(val)) |
156
|
|
|
return "".join(ret) |
157
|
|
|
|
158
|
|
|
def fcgi_type_begin(): |
159
|
|
|
"""Return a FCGI_BeginRequestRecord Responder request. |
160
|
|
|
|
161
|
|
|
:rtype: str |
162
|
|
|
""" |
163
|
|
|
header = struct.pack("!BBHHBx", FCGI_VERSION_1, FCGI_BEGIN_REQUEST, |
164
|
|
|
1, FCGI_BEGIN_REQUEST_LEN, 0) |
165
|
|
|
body = struct.pack('!HB5x', FCGI_RESPONDER, 0) |
166
|
|
|
return "".join([header, body]) |
167
|
|
|
|
168
|
|
|
def fcgi_type_params(params): |
169
|
|
|
"""Return a FCGI_PARAMS record. |
170
|
|
|
|
171
|
|
|
:param params: A str returned from fcgi_pack. |
172
|
|
|
:type params: str |
173
|
|
|
:rtype: str |
174
|
|
|
""" |
175
|
|
|
header = struct.pack("!BBHHBx", FCGI_VERSION_1, FCGI_PARAMS, |
176
|
|
|
1, len(params), 0) |
177
|
|
|
return "".join([header, params]) |
178
|
|
|
|
179
|
|
|
def fcgi_type_data(data): |
180
|
|
|
"""Return a FCGI_DATA record. |
181
|
|
|
|
182
|
|
|
:param data: A payload string |
183
|
|
|
:type data: str |
184
|
|
|
:rtype: str |
185
|
|
|
""" |
186
|
|
|
header = struct.pack("!BBHHBx", FCGI_VERSION_1, FCGI_DATA, |
187
|
|
|
1, len(data), 0) |
188
|
|
|
return "".join([header, data]) |
189
|
|
|
|
190
|
|
|
|
191
|
|
|
def fcgi_type_stdin(data): |
192
|
|
|
"""Return a FCGI_STDIN record. |
193
|
|
|
|
194
|
|
|
:param data: The data to send to stdin |
195
|
|
|
:type data: str |
196
|
|
|
:rtype: str |
197
|
|
|
""" |
198
|
|
|
header = struct.pack("!BBHHBx", FCGI_VERSION_1, FCGI_STDIN, |
199
|
|
|
1, len(data), 0) |
200
|
|
|
return "".join([header, data]) |
201
|
|
|
|
202
|
|
|
|
203
|
|
|
class FcgiNet(object): |
204
|
|
|
"""A socket reader/writer that reads/writes FastCGI records.""" |
205
|
|
|
|
206
|
|
|
def __init__(self, addr, timeout): |
207
|
|
|
"""A helper class to read/write FcgiHeaders to/from a server. |
208
|
|
|
|
209
|
|
|
:param addr: The FastCGI host:port to connect to. |
210
|
|
|
:type addr: str |
211
|
|
|
:param to: An optional timeout |
212
|
|
|
:type to: int |
213
|
|
|
:rtype: FcgiNet |
214
|
|
|
""" |
215
|
|
|
self.addr = addr |
216
|
|
|
self.timeout = timeout |
217
|
|
|
self.socket = None |
218
|
|
|
|
219
|
|
|
def connect(self): |
220
|
|
|
if self.socket: |
221
|
|
|
self.socket.close() |
222
|
|
|
try: |
223
|
|
|
# connect |
224
|
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
225
|
|
|
# set timeout for socket operations |
226
|
|
|
self.socket.settimeout(self.timeout) |
227
|
|
|
self.socket.connect(self.addr) |
228
|
|
|
except Exception as e: |
229
|
|
|
msg = "could not connect: {0}" |
230
|
|
|
raise FcgiRequestFailed(msg.format(e)) |
231
|
|
|
|
232
|
|
|
def rx_nbytes(self, nbytes): |
233
|
|
|
"""Read nbytes from our socket, return a tuple of the number of bytes |
234
|
|
|
read and the data read. |
235
|
|
|
|
236
|
|
|
:param nbytes: The number of bytes to attempt to read |
237
|
|
|
:type nbytes: int |
238
|
|
|
|
239
|
|
|
:rtype: tuple(int, str) |
240
|
|
|
:raises: FcgiRequestFailed |
241
|
|
|
""" |
242
|
|
|
dat = deque() |
243
|
|
|
nbytes_rx = 0 |
244
|
|
|
while nbytes_rx < nbytes: |
245
|
|
|
buff = None |
246
|
|
|
try: |
247
|
|
|
buff = self.socket.recv(nbytes) |
248
|
|
|
except socket.error as e: |
249
|
|
|
msg = "Exception reading from remote host {0}:{1}: {2}" |
250
|
|
|
msg = msg.format(self.addr[0], self.addr[1], e) |
251
|
|
|
raise FcgiRequestFailed(msg) |
252
|
|
|
if not buff: |
253
|
|
|
break |
254
|
|
|
dat.append(buff) |
255
|
|
|
nbytes_rx += len(buff) |
256
|
|
|
return (nbytes_rx, "".join(dat)) |
257
|
|
|
|
258
|
|
|
def rx(self): |
259
|
|
|
"""Read a FastCGI header and payload from our socket and return it as a |
260
|
|
|
a tuple of (header, buffer). |
261
|
|
|
|
262
|
|
|
:raises: |
263
|
|
|
FcgiRequestFailed |
264
|
|
|
|
265
|
|
|
:rtype: tuple(FcgiHeader, dict) |
266
|
|
|
""" |
267
|
|
|
# hdr.data will raise ValueError if data length != FCGI_HEADER_LEN |
268
|
|
|
hlen, dat = self.rx_nbytes(FCGI_HEADER_LEN) |
269
|
|
|
if hlen != FCGI_HEADER_LEN: |
270
|
|
|
msg = "Excpecting header of {0} bytes, received {1}" |
271
|
|
|
raise FcgiRequestFailed(msg.format(FCGI_HEADER_LEN, hlen)) |
272
|
|
|
|
273
|
|
|
hdr = fcgi_header(dat) |
274
|
|
|
dat = "" |
275
|
|
|
dat_len = hdr['data_len'] |
276
|
|
|
pad_len = hdr['pad_len'] |
277
|
|
|
|
278
|
|
|
if dat_len: |
279
|
|
|
dlen, dat = self.rx_nbytes(dat_len) |
280
|
|
|
if dlen != dat_len: |
281
|
|
|
msg = "Excpecting data of {0} bytes, received {1}" |
282
|
|
|
raise FcgiRequestFailed(msg.format(dat_len, dlen)) |
283
|
|
|
|
284
|
|
|
if pad_len: |
285
|
|
|
plen, pad = self.rx_nbytes(pad_len) |
286
|
|
|
if plen != pad_len: |
287
|
|
|
msg = "Excpecting padding of {0} bytes, received {1}" |
288
|
|
|
raise FcgiRequestFailed(msg.format(pad_len, plen)) |
289
|
|
|
|
290
|
|
|
return (hdr, dat) |
291
|
|
|
|
292
|
|
|
def tx(self, dat): |
293
|
|
|
"""Send a FastCGI Record to our socket. |
294
|
|
|
|
295
|
|
|
:param record: The FcgiRecord to send |
296
|
|
|
:type record: FcgiRecord |
297
|
|
|
""" |
298
|
|
|
if not self.socket: |
299
|
|
|
self.connect() |
300
|
|
|
self.socket.sendall(dat) |
301
|
|
|
|
302
|
|
|
def end(self): |
303
|
|
|
"""Close the socket.""" |
304
|
|
|
if self.socket: |
305
|
|
|
self.socket.close() |
306
|
|
|
|
307
|
|
|
|
308
|
|
|
def fcgi_call(addr, timeout, script, root, query): |
309
|
|
|
"""Call <script> on local/remote FastCGI server <addr> and return results. |
310
|
|
|
|
311
|
|
|
The script is called with no arguments and minimal FastCGI parameters |
312
|
|
|
are set. |
313
|
|
|
|
314
|
|
|
:param addr: The host:port to connect to |
315
|
|
|
:type addr: str |
316
|
|
|
:param timeout: The tcp timeout to set on the socket |
317
|
|
|
:type timeout: int |
318
|
|
|
:param script: The name of the script to call |
319
|
|
|
:type script: str |
320
|
|
|
:param root: The root directory the script lives in |
321
|
|
|
:type root: str |
322
|
|
|
:param query: The query string for the request (eg. json&full) |
323
|
|
|
:type query: str |
324
|
|
|
:rval: tuple() |
325
|
|
|
:raises: FcgiRequestFailed |
326
|
|
|
""" |
327
|
|
|
net = FcgiNet(addr, timeout) |
328
|
|
|
sout = deque() |
329
|
|
|
serr = deque() |
330
|
|
|
r_begin = fcgi_type_begin() |
331
|
|
|
params = { |
332
|
|
|
"QUERY_STRING": "json", |
333
|
|
|
"REQUEST_METHOD": "GET", |
334
|
|
|
"CONTENT_TYPE": "", |
335
|
|
|
"CONTENT_LENGTH": "", |
336
|
|
|
"SCRIPT_NAME": script, |
337
|
|
|
"REQUEST_URI": script, |
338
|
|
|
"DOCUMENT_ROOT": root, |
339
|
|
|
"SERVER_PROTOCOL": "HTTP/1.1", |
340
|
|
|
"GATEWAY_INTERFACE": "CGI/1.1", |
341
|
|
|
"SERVER_SOFTWARE": "fcgi/1.0", |
342
|
|
|
"REMOTE_ADDR": "127.0.0.1", |
343
|
|
|
"REMOTE_PORT": "0", |
344
|
|
|
"SERVER_ADDR": "127.0.0.1", |
345
|
|
|
"SERVER_PORT": "80", |
346
|
|
|
"SERVER_NAME": "localhost", |
347
|
|
|
"PATH_INFO": script, |
348
|
|
|
"SCRIPT_FILENAME": "{0}/{1}".format(root, script) |
349
|
|
|
} |
350
|
|
|
param_dat = fcgi_pack(params) |
351
|
|
|
r_params = fcgi_type_params(param_dat) |
352
|
|
|
r_stdin = fcgi_type_stdin("") |
353
|
|
|
net.tx("".join([r_begin, r_params, r_stdin])) |
354
|
|
|
while True: |
355
|
|
|
hdr, buff = net.rx() |
356
|
|
|
if hdr['type'] == FCGI_STDOUT: |
357
|
|
|
sout.append(buff) |
358
|
|
|
continue |
359
|
|
|
elif hdr['type'] == FCGI_STDERR: |
360
|
|
|
serr.append(buff) |
361
|
|
|
continue |
362
|
|
|
elif hdr['type'] == FCGI_END_REQUEST: |
363
|
|
|
net.end() |
364
|
|
|
astatus, pstatus = fcgi_end_request(buff) |
365
|
|
|
if pstatus != FCGI_REQUEST_COMPLETE or astatus != 0: |
366
|
|
|
msg = "bad proto/app status: {0}, {1}".format(pstatus, astatus) |
367
|
|
|
raise FcgiRequestFailed(msg) |
368
|
|
|
break |
369
|
|
|
net.end() |
370
|
|
|
msg = "unexpected response: {0} : {1}" |
371
|
|
|
raise FcgiRequestFailed(msg.format(hdr, buff)) |
372
|
|
|
net.end() |
373
|
|
|
return ("".join(sout), "".join(serr)) |
374
|
|
|
|
375
|
|
|
|
376
|
|
|
|
377
|
|
|
class Php(plumd.plugins.Reader): |
378
|
|
|
"""Plugin to record php-fpm, opcache and apc metrics.""" |
379
|
|
|
|
380
|
|
|
# default config values |
381
|
|
|
defaults = { |
382
|
|
|
'poll.interval': 10, |
383
|
|
|
'fpm_status': "/fpmstatus", # match pm.status_path from config |
384
|
|
|
'fpm_status_args': "json", # query params (json required) |
385
|
|
|
'fpm_pools': { |
386
|
|
|
'www': { # add an entry per pool to monitor |
387
|
|
|
'host': '127.0.0.1', |
388
|
|
|
'port': 9000, |
389
|
|
|
'script': 'cacheinfo.php', |
390
|
|
|
'root': '/var/www' |
391
|
|
|
} |
392
|
|
|
}, |
393
|
|
|
'record': { |
394
|
|
|
'apc': [ "expunges","mem_size", "num_entries", "num_hits", |
395
|
|
|
"num_inserts", "num_misses", "num_slots", "ttl" ], |
396
|
|
|
'apc_sma': [ "avail_mem", "num_seg", "seg_size" ] |
397
|
|
|
}, |
398
|
|
|
'record_op': { |
399
|
|
|
"interned_strings_usage": [ |
400
|
|
|
"buffer_size", "free_memory", "number_of_strings", |
401
|
|
|
"used_memory" |
402
|
|
|
], |
403
|
|
|
"memory_usage": [ |
404
|
|
|
"current_wasted_percentage", "free_memory", "used_memory", "wasted_memory" |
405
|
|
|
], |
406
|
|
|
"opcache_statistics": [ |
407
|
|
|
"blacklist_miss_ratio", "blacklist_misses", "hash_restarts", |
408
|
|
|
"hits", "manual_restarts", "max_cached_keys", "misses", |
409
|
|
|
"num_cached_keys","num_cached_scripts", "oom_restarts", |
410
|
|
|
"opcache_hit_rate" |
411
|
|
|
] |
412
|
|
|
}, |
413
|
|
|
'record_status': [ |
414
|
|
|
"accepted conn", "active processes", "idle processes", |
415
|
|
|
"listen queue", "listen queue len", "max active processes", |
416
|
|
|
"max children reached", "max listen queue", "slow requests", |
417
|
|
|
"total processes" |
418
|
|
|
], |
419
|
|
|
'rename': { |
420
|
|
|
'interned_strings_usage': 'strings', |
421
|
|
|
'memory_usage': 'mem', |
422
|
|
|
'opcache_statistics': 'cache', |
423
|
|
|
'buffer_size': 'buf_size', |
424
|
|
|
'free_memory': 'mem_free', |
425
|
|
|
'number_of_strings': 'num_str', |
426
|
|
|
'used_memory': 'mem_used', |
427
|
|
|
'current_wasted_percentage': 'waste_perc', |
428
|
|
|
'wasted_memory': 'mem_wasted', |
429
|
|
|
'blacklist_miss_ratio': 'bl_miss_ratio', |
430
|
|
|
'blacklist_misses': 'bl_miss', |
431
|
|
|
'hash_restarts': 'h_restarts', |
432
|
|
|
'manual_restarts': 'm_restarts', |
433
|
|
|
'max_cached_keys': 'max_keys', |
434
|
|
|
'num_cached_keys': 'num_keys', |
435
|
|
|
'num_cached_scripts': 'num_scripts', |
436
|
|
|
'opcache_hit_rate': 'hit_rate', |
437
|
|
|
'accepted conn': 'con_accepted', |
438
|
|
|
'active processes': 'proc_active', |
439
|
|
|
'idle processes': 'proc_idle', |
440
|
|
|
'listen queue': 'listen_q', |
441
|
|
|
'listen queue len': 'listen_q_len', |
442
|
|
|
'max active processes': 'proc_max_active', |
443
|
|
|
'max children reached': 'proc_max_child', |
444
|
|
|
'max listen queue': 'listen_q_max', |
445
|
|
|
'slow requests': 'req_slow', |
446
|
|
|
'total processes': 'proc_total' |
447
|
|
|
}, |
448
|
|
|
'timeout': 10 |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
def __init__(self, log, config): |
452
|
|
|
"""Plugin to record nginx stub_status metrics. |
453
|
|
|
|
454
|
|
|
:param log: A logger |
455
|
|
|
:type log: logging.RootLogger |
456
|
|
|
:param config: a plumd.config.Conf configuration helper instance. |
457
|
|
|
:type config: plumd.config.Conf |
458
|
|
|
""" |
459
|
|
|
super(Php, self).__init__(log, config) |
460
|
|
|
self.config.defaults(Php.defaults) |
461
|
|
|
|
462
|
|
|
self.fpm_status = self.config.get('fpm_status') |
463
|
|
|
self.fpm_status_args = self.config.get('fpm_status_args') |
464
|
|
|
self.record_status = self.config.get('record_status') |
465
|
|
|
self.pools = self.config.get('pools') |
466
|
|
|
self.timeout = self.config.get('timeout') |
467
|
|
|
self.record = self.config.get("record") |
468
|
|
|
self.record_op = self.config.get("record_op") |
469
|
|
|
self.rename = self.config.get("rename") |
470
|
|
|
|
471
|
|
|
|
472
|
|
|
|
473
|
|
|
def poll(self): |
474
|
|
|
"""Query PHP-FPM for metrics over a FastCGI connection. |
475
|
|
|
|
476
|
|
|
Records php-fpm pool stats from pm.status_path and also |
477
|
|
|
output from a php script that returns both opcache and apc metrics. |
478
|
|
|
|
479
|
|
|
The php-fpm pool stats output is expected to be in json by setting a |
480
|
|
|
QUERY_STRING FastCGI parameter to json (currently does not support |
481
|
|
|
json&full). |
482
|
|
|
|
483
|
|
|
The opcache and apc metrics are parsed from a custom php script - see |
484
|
|
|
misc in the git repo for the script source. |
485
|
|
|
|
486
|
|
|
:rtype: ResultSet |
487
|
|
|
""" |
488
|
|
|
result = plumd.Result("php") |
489
|
|
|
record = self.config.get('record') |
490
|
|
|
record_op = self.config.get('record_op') |
491
|
|
|
rename = self.config.get('rename') |
492
|
|
|
timeout = self.config.get('timeout') |
493
|
|
|
record_status = self.config.get('record_status') |
494
|
|
|
fpm_status = self.config.get('fpm_status') |
495
|
|
|
fpm_status_args = self.config.get('fpm_status_args') |
496
|
|
|
fpm_pools = self.config.get('fpm_pools') |
497
|
|
|
|
498
|
|
|
# check each configured pool |
499
|
|
|
for pname, pconf in fpm_pools.items(): |
500
|
|
|
try: |
501
|
|
|
addr = (pconf['host'], pconf['port']) |
502
|
|
|
script = pconf['script'] |
503
|
|
|
root = pconf['root'] |
504
|
|
|
except KeyError as e: |
505
|
|
|
msg = "Php: pool {0} config invalid: {1}" |
506
|
|
|
self.log.error(msg.format(pname, e)) |
507
|
|
|
continue |
508
|
|
|
# call the status script and get its output |
509
|
|
|
cinfo = {} |
510
|
|
|
try: |
511
|
|
|
sout, serr = fcgi_call(addr, timeout, fpm_status, root, |
512
|
|
|
fpm_status_args) |
513
|
|
|
lines = [ line for line in sout.split("\n") |
514
|
|
|
if line.startswith("{\"") ] |
515
|
|
|
cinfo = json.loads("".join(lines)) |
516
|
|
|
except Exception as e: |
517
|
|
|
msg = "Php: failed to call {0} for pool {1}: {2}" |
518
|
|
|
self.log.error(msg.format(fpm_status, pname, e)) |
519
|
|
|
continue |
520
|
|
|
|
521
|
|
|
# record fpm status metrics |
522
|
|
|
for metric in record_status: |
523
|
|
|
if metric not in cinfo: |
524
|
|
|
continue |
525
|
|
|
mname = metric if metric not in rename else rename[metric] |
526
|
|
|
mstr = "{0}.fpm.{1}".format(pname, mname) |
527
|
|
|
result.add(plumd.Float(mstr, cinfo[metric])) |
528
|
|
|
|
529
|
|
|
# call the cache info script and get its output |
530
|
|
|
cinfo = {} |
531
|
|
|
try: |
532
|
|
|
sout, serr = fcgi_call(addr, timeout, script, root, "") |
533
|
|
|
lines = [ line for line in sout.split("\n") |
534
|
|
|
if line.startswith("{\"") ] |
535
|
|
|
cinfo = json.loads("".join(lines)) |
536
|
|
|
except Exception as e: |
537
|
|
|
msg = "Php: failed to call {0} for pool {1}: {2}" |
538
|
|
|
self.log.error(msg.format(script, pname, e)) |
539
|
|
|
continue |
540
|
|
|
|
541
|
|
|
# apc |
542
|
|
|
for key, metrics in record.items(): |
543
|
|
|
if key not in cinfo: |
544
|
|
|
continue |
545
|
|
|
mkey = key if key not in rename else rename[key] |
546
|
|
|
for metric in metrics: |
547
|
|
|
if metric not in cinfo[key]: |
548
|
|
|
continue |
549
|
|
|
mname = metric if metric not in rename else rename[metric] |
550
|
|
|
mstr = "{0}.{1}.{2}".format(pname, mkey, mname) |
551
|
|
|
result.add(plumd.Float(mstr, cinfo[key][metric])) |
552
|
|
|
|
553
|
|
|
# opcache |
554
|
|
|
for key, metrics in record_op.items(): |
555
|
|
|
if key not in cinfo['op']: |
556
|
|
|
continue |
557
|
|
|
mkey = ke if key not in rename else rename[key] |
558
|
|
|
for metric in metrics: |
559
|
|
|
if metric not in cinfo['op'][key]: |
560
|
|
|
continue |
561
|
|
|
mname = metric if metric not in rename else rename[metric] |
562
|
|
|
mstr = "{0}.op.{1}.{2}".format(pname, mkey, mname) |
563
|
|
|
result.add(plumd.Float(mstr, cinfo['op'][key][metric])) |
564
|
|
|
|
565
|
|
|
return plumd.ResultSet([result]) |
566
|
|
|
|
567
|
|
|
|
568
|
|
|
""" |
569
|
|
|
Example php script and pm.status_path outputs: |
570
|
|
|
|
571
|
|
|
Cache output: |
572
|
|
|
{ |
573
|
|
|
"apc": { |
574
|
|
|
"expunges": 0, |
575
|
|
|
"file_upload_progress": 1, |
576
|
|
|
"mem_size": 65600, |
577
|
|
|
"memory_type": "mmap", |
578
|
|
|
"num_entries": 100, |
579
|
|
|
"num_hits": 0, |
580
|
|
|
"num_inserts": 100, |
581
|
|
|
"num_misses": 0, |
582
|
|
|
"num_slots": 4099, |
583
|
|
|
"start_time": 1472356538, |
584
|
|
|
"ttl": 0 |
585
|
|
|
}, |
586
|
|
|
"op": { |
587
|
|
|
"cache_full": false, |
588
|
|
|
"interned_strings_usage": { |
589
|
|
|
"buffer_size": 8388608, |
590
|
|
|
"free_memory": 8050800, |
591
|
|
|
"number_of_strings": 3748, |
592
|
|
|
"used_memory": 337808 |
593
|
|
|
}, |
594
|
|
|
"memory_usage": { |
595
|
|
|
"current_wasted_percentage": 0.0081896781921387, |
596
|
|
|
"free_memory": 123263200, |
597
|
|
|
"used_memory": 10943536, |
598
|
|
|
"wasted_memory": 10992 |
599
|
|
|
}, |
600
|
|
|
"opcache_enabled": true, |
601
|
|
|
"opcache_statistics": { |
602
|
|
|
"blacklist_miss_ratio": 0, |
603
|
|
|
"blacklist_misses": 0, |
604
|
|
|
"hash_restarts": 0, |
605
|
|
|
"hits": 90, |
606
|
|
|
"last_restart_time": 0, |
607
|
|
|
"manual_restarts": 0, |
608
|
|
|
"max_cached_keys": 7963, |
609
|
|
|
"misses": 5, |
610
|
|
|
"num_cached_keys": 2, |
611
|
|
|
"num_cached_scripts": 2, |
612
|
|
|
"oom_restarts": 0, |
613
|
|
|
"opcache_hit_rate": 94.736842105263, |
614
|
|
|
"start_time": 1472356538 |
615
|
|
|
}, |
616
|
|
|
"restart_in_progress": false, |
617
|
|
|
"restart_pending": false |
618
|
|
|
} |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
|
622
|
|
|
|
623
|
|
|
Cache Output w/ SMA: |
624
|
|
|
{ |
625
|
|
|
"apc": { |
626
|
|
|
"expunges": 0, |
627
|
|
|
"file_upload_progress": 1, |
628
|
|
|
"mem_size": 65600, |
629
|
|
|
"memory_type": "mmap", |
630
|
|
|
"num_entries": 100, |
631
|
|
|
"num_hits": 0, |
632
|
|
|
"num_inserts": 100, |
633
|
|
|
"num_misses": 0, |
634
|
|
|
"num_slots": 4099, |
635
|
|
|
"start_time": 1472356538, |
636
|
|
|
"ttl": 0 |
637
|
|
|
}, |
638
|
|
|
"apc_sma": { |
639
|
|
|
"avail_mem": 33452504, |
640
|
|
|
"num_seg": 1, |
641
|
|
|
"seg_size": 33554296 |
642
|
|
|
}, |
643
|
|
|
"op": { |
644
|
|
|
"cache_full": false, |
645
|
|
|
"interned_strings_usage": { |
646
|
|
|
"buffer_size": 8388608, |
647
|
|
|
"free_memory": 8050800, |
648
|
|
|
"number_of_strings": 3748, |
649
|
|
|
"used_memory": 337808 |
650
|
|
|
}, |
651
|
|
|
"memory_usage": { |
652
|
|
|
"current_wasted_percentage": 0.0081896781921387, |
653
|
|
|
"free_memory": 123263200, |
654
|
|
|
"used_memory": 10943536, |
655
|
|
|
"wasted_memory": 10992 |
656
|
|
|
}, |
657
|
|
|
"opcache_enabled": true, |
658
|
|
|
"opcache_statistics": { |
659
|
|
|
"blacklist_miss_ratio": 0, |
660
|
|
|
"blacklist_misses": 0, |
661
|
|
|
"hash_restarts": 0, |
662
|
|
|
"hits": 90, |
663
|
|
|
"last_restart_time": 0, |
664
|
|
|
"manual_restarts": 0, |
665
|
|
|
"max_cached_keys": 7963, |
666
|
|
|
"misses": 5, |
667
|
|
|
"num_cached_keys": 2, |
668
|
|
|
"num_cached_scripts": 2, |
669
|
|
|
"oom_restarts": 0, |
670
|
|
|
"opcache_hit_rate": 94.736842105263, |
671
|
|
|
"start_time": 1472356538 |
672
|
|
|
}, |
673
|
|
|
"restart_in_progress": false, |
674
|
|
|
"restart_pending": false |
675
|
|
|
} |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
|
679
|
|
|
|
680
|
|
|
|
681
|
|
|
|
682
|
|
|
Normal output: |
683
|
|
|
X-Powered-By: PHP/5.4.16 |
684
|
|
|
Expires: Thu, 01 Jan 1970 00:00:00 GMT |
685
|
|
|
Cache-Control: no-cache, no-store, must-revalidate, max-age=0 |
686
|
|
|
Content-Type: application/json |
687
|
|
|
|
688
|
|
|
{ |
689
|
|
|
"accepted conn": 164, |
690
|
|
|
"active processes": 1, |
691
|
|
|
"idle processes": 5, |
692
|
|
|
"listen queue": 0, |
693
|
|
|
"listen queue len": 128, |
694
|
|
|
"max active processes": 1, |
695
|
|
|
"max children reached": 0, |
696
|
|
|
"max listen queue": 0, |
697
|
|
|
"pool": "www", |
698
|
|
|
"process manager": "dynamic", |
699
|
|
|
"slow requests": 0, |
700
|
|
|
"start since": 16150, |
701
|
|
|
"start time": 1472356538, |
702
|
|
|
"total processes": 6 |
703
|
|
|
} |
704
|
|
|
|
705
|
|
|
|
706
|
|
|
Full output: |
707
|
|
|
X-Powered-By: PHP/5.4.16 |
708
|
|
|
Expires: Thu, 01 Jan 1970 00:00:00 GMT |
709
|
|
|
Cache-Control: no-cache, no-store, must-revalidate, max-age=0 |
710
|
|
|
Content-Type: application/json |
711
|
|
|
|
712
|
|
|
{ |
713
|
|
|
"accepted conn": 162, |
714
|
|
|
"active processes": 1, |
715
|
|
|
"idle processes": 5, |
716
|
|
|
"listen queue": 0, |
717
|
|
|
"listen queue len": 128, |
718
|
|
|
"max active processes": 1, |
719
|
|
|
"max children reached": 0, |
720
|
|
|
"max listen queue": 0, |
721
|
|
|
"pool": "www", |
722
|
|
|
"process manager": "dynamic", |
723
|
|
|
"processes": [ |
724
|
|
|
{ |
725
|
|
|
"content length": 0, |
726
|
|
|
"last request cpu": 0.0, |
727
|
|
|
"last request memory": 262144, |
728
|
|
|
"pid": 13170, |
729
|
|
|
"request duration": 790, |
730
|
|
|
"request method": "GET", |
731
|
|
|
"request uri": "/fpmstatus?json&full", |
732
|
|
|
"requests": 29, |
733
|
|
|
"script": "-", |
734
|
|
|
"start since": 15954, |
735
|
|
|
"start time": 1472356538, |
736
|
|
|
"state": "Idle", |
737
|
|
|
"user": "-" |
738
|
|
|
}, |
739
|
|
|
{ |
740
|
|
|
"content length": 0, |
741
|
|
|
"last request cpu": 0.0, |
742
|
|
|
"last request memory": 0, |
743
|
|
|
"pid": 13171, |
744
|
|
|
"request duration": 219, |
745
|
|
|
"request method": "GET", |
746
|
|
|
"request uri": "/fpmstatus?json&full", |
747
|
|
|
"requests": 29, |
748
|
|
|
"script": "-", |
749
|
|
|
"start since": 15954, |
750
|
|
|
"start time": 1472356538, |
751
|
|
|
"state": "Running", |
752
|
|
|
"user": "-" |
753
|
|
|
}, |
754
|
|
|
{ |
755
|
|
|
"content length": 0, |
756
|
|
|
"last request cpu": 3802.28, |
757
|
|
|
"last request memory": 262144, |
758
|
|
|
"pid": 13172, |
759
|
|
|
"request duration": 263, |
760
|
|
|
"request method": "GET", |
761
|
|
|
"request uri": "/fpmstatus?json", |
762
|
|
|
"requests": 28, |
763
|
|
|
"script": "-", |
764
|
|
|
"start since": 15954, |
765
|
|
|
"start time": 1472356538, |
766
|
|
|
"state": "Idle", |
767
|
|
|
"user": "-" |
768
|
|
|
}, |
769
|
|
|
{ |
770
|
|
|
"content length": 0, |
771
|
|
|
"last request cpu": 0.0, |
772
|
|
|
"last request memory": 262144, |
773
|
|
|
"pid": 13173, |
774
|
|
|
"request duration": 216, |
775
|
|
|
"request method": "GET", |
776
|
|
|
"request uri": "/fpmstatus?json", |
777
|
|
|
"requests": 28, |
778
|
|
|
"script": "-", |
779
|
|
|
"start since": 15954, |
780
|
|
|
"start time": 1472356538, |
781
|
|
|
"state": "Idle", |
782
|
|
|
"user": "-" |
783
|
|
|
}, |
784
|
|
|
{ |
785
|
|
|
"content length": 0, |
786
|
|
|
"last request cpu": 0.0, |
787
|
|
|
"last request memory": 262144, |
788
|
|
|
"pid": 13174, |
789
|
|
|
"request duration": 466, |
790
|
|
|
"request method": "GET", |
791
|
|
|
"request uri": "/fpmstatus?json&full", |
792
|
|
|
"requests": 28, |
793
|
|
|
"script": "-", |
794
|
|
|
"start since": 15954, |
795
|
|
|
"start time": 1472356538, |
796
|
|
|
"state": "Idle", |
797
|
|
|
"user": "-" |
798
|
|
|
}, |
799
|
|
|
{ |
800
|
|
|
"content length": 0, |
801
|
|
|
"last request cpu": 0.0, |
802
|
|
|
"last request memory": 262144, |
803
|
|
|
"pid": 15823, |
804
|
|
|
"request duration": 780, |
805
|
|
|
"request method": "GET", |
806
|
|
|
"request uri": "/fpmstatus?json&full", |
807
|
|
|
"requests": 20, |
808
|
|
|
"script": "-", |
809
|
|
|
"start since": 875, |
810
|
|
|
"start time": 1472371617, |
811
|
|
|
"state": "Idle", |
812
|
|
|
"user": "-" |
813
|
|
|
} |
814
|
|
|
], |
815
|
|
|
"slow requests": 0, |
816
|
|
|
"start since": 15954, |
817
|
|
|
"start time": 1472356538, |
818
|
|
|
"total processes": 6 |
819
|
|
|
} |
820
|
|
|
""" |
821
|
|
|
|