|
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
|
|
|
|