Display PHP-FPM (FastCGI Process Manager) pool information using UNIX socket and Python script. I thought it would be easy to write a simple FCGI client. Apparently not, but it was an exciting and rewarding experience.
I have already described how to enable the PHP-FPM status page, so I will skip this step today.
Look at the end of this blog post for additional information, including FastCGI specification.
Python script
It is the most straightforward implementation without external dependencies.
#!/usr/bin/env python import sys import socket import struct class FCGIStatusClient: # FCGI protocol version FCGI_VERSION = 1 # FCGI record types FCGI_BEGIN_REQUEST = 1 FCGI_PARAMS = 4 # FCGI roles FCGI_RESPONDER = 1 # FCGI header length FCGI_HEADER_LENGTH = 8 def __init__(self, socket_path = "/var/run/php/php7.0-fpm.sock", socket_timeout = 5.0, status_path = "/status" ): self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.socket_path = socket_path self.set_socket_timeout(socket_timeout) self.status_path = status_path self.request_id = 1 self.params = { "SCRIPT_NAME": status_path, "SCRIPT_FILENAME": status_path, "QUERY_STRING": "", "REQUEST_METHOD": "GET", } def set_socket_timeout(self, timeout): self.socket_timeout = timeout self.socket.settimeout(self.socket_timeout) def connect(self): try: self.socket.connect(self.socket_path) except: print(sys.exc_info()[1]) sys.exit(1) def close(self): self.socket.close() def define_begin_request(self): fcgi_begin_request = struct.pack("!HB5x", self.FCGI_RESPONDER, 0) fcgi_header = struct.pack("!BBHHBx", self.FCGI_VERSION, self.FCGI_BEGIN_REQUEST, self.request_id, len(fcgi_begin_request), 0) self.fcgi_begin_request = fcgi_header + fcgi_begin_request def define_parameters(self): parameters = [] for name, value in self.params.items(): parameters.append(chr(len(name)) + chr(len(value)) + name + value) parameters = ''.join(parameters) parameters_length = len(parameters) parameters_padding_req = parameters_length & 7 parameters_padding = b'\x00' * parameters_padding_req fcgi_header_start = struct.pack("!BBHHBx", self.FCGI_VERSION, self.FCGI_PARAMS, self.request_id, parameters_length , parameters_padding_req) fcgi_header_end = struct.pack("!BBHHBx", self.FCGI_VERSION, self.FCGI_PARAMS, self.request_id, 0, 0) self.fcgi_params = fcgi_header_start + parameters.encode() + parameters_padding + fcgi_header_end def execute(self): try: self.socket.send(self.fcgi_begin_request) self.socket.send(self.fcgi_params) header = self.socket.recv(self.FCGI_HEADER_LENGTH) fcgi_version, request_type, request_id, request_length, request_padding = struct.unpack("!BBHHBx", header) if request_type == 6: self.raw_status_data=self.socket.recv(request_length) else: self.raw_status_data = "" if request_type == 7: raise Exception("Received an error packet.") else: raise Exception("Received unexpected packet type.") except: print(sys.exc_info()[1]) sys.exit(2) self.status_data = self.raw_status_data.decode().split("\r\n\r\n")[1] def make_request(self): self.define_begin_request() self.define_parameters() self.connect() self.execute() self.close() def print_status(self): print(self.status_data) fcgi_client = FCGIStatusClient( socket_path = "/var/run/php/php7.0-fpm.sock", status_path = "/status" ) fcgi_client.make_request() fcgi_client.print_status()
Sample usage
This script returns pool statistics and can be easily extended to report data using the desired format.
$ sudo -u www-data /usr/local/bin/fcgi_status.py pool: www process manager: dynamic start time: 15/Aug/2018:14:29:57 +0000 start since: 114173 accepted conn: 392 listen queue: 0 max listen queue: 0 listen queue len: 0 idle processes: 1 active processes: 1 total processes: 2 max active processes: 2 max children reached: 0 slow requests: 0