How to monitor PHP-FPM pool

Monitor PHP-FPM (FastCGI Process Manager) pool using curl utility, cgi-fcgi application or slightly modified Python script that was used to display PHP-FPM pool information.

Enable PHP-FPM ping page for a specific pool

Edit PHP-FPM pool configuration file to define status ping URI and respone.

$ sudo vim /etc/php/7.0/fpm/pool.d/www.conf
[...]
ping.path     = /ruok
ping.response = imok
[...]

By default the ping page is not enabled as URI is not defined.

You can assume that default challenge and response is simply ping → pong.

Reload PHP-FPM service to apply the configuration change.

$ sudo systemctl reload php7.0-fpm

Serve PHP-FPM ping page for a specific pool

Use the following NGINX configuration snippet to ensure that status page is available only on local machine.

  location ~ ^/ruok$ {
    allow 127.0.0.1/32;
    deny all;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
  }

You can just deny all requests which is a better solution or just extend this solution to use basic authentication or other additional measures to restict access.

Use curl utility to access ping page

Inspect challenge and response mechanism using curl.

$ curl -4k https://localhost/ruok
imok

Use the following command to perform specific operation that depends on the received response.

$ curl -4sk https://localhost/ruok | awk '{if ($0 == "imok") {exit 0} else {exit 1}}' && (echo "green") || (echo "red")

It will display green or red status code, so you can easily modify it.

Use CGI/1.1 program to monitor FastCGI server using UNIX socket

Install shared FastCGI library that includes a cgi-fcgi utility.

$ sudo apt-get install -y libfcgi0ldbl
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  libfcgi-bin
The following NEW packages will be installed:
  libfcgi-bin libfcgi0ldbl
0 upgraded, 2 newly installed, 0 to remove and 4 not upgraded.
Need to get 162 kB of archives.
After this operation, 483 kB of additional disk space will be used.
Get:1 http://ftp.icm.edu.pl/pub/Linux/distributions/raspbian/raspbian stretch/main armhf libfcgi0ldbl armhf 2.4.0-8.4 [151 kB]
Get:2 http://ftp.icm.edu.pl/pub/Linux/distributions/raspbian/raspbian stretch/main armhf libfcgi-bin
armhf 2.4.0-8.4 [11.2 kB]
Fetched 162 kB in 0s (266 kB/s)
Selecting previously unselected package libfcgi0ldbl:armhf.
(Reading database ... 38762 files and directories currently installed.)
Preparing to unpack .../libfcgi0ldbl_2.4.0-8.4_armhf.deb ...
Unpacking libfcgi0ldbl:armhf (2.4.0-8.4) ...
Selecting previously unselected package libfcgi-bin.
Preparing to unpack .../libfcgi-bin_2.4.0-8.4_armhf.deb ...
Unpacking libfcgi-bin (2.4.0-8.4) ...
Setting up libfcgi0ldbl:armhf (2.4.0-8.4) ...
Processing triggers for libc-bin (2.24-11+deb9u3) ...
Processing triggers for man-db (2.7.6.1-2) ...
Setting up libfcgi-bin (2.4.0-8.4) ...

Display PHP-FPM pool information.

$ sudo -u www-data bash -c "export SCRIPT_NAME=/ruok; export SCRIPT_FILENAME=/ruok; export REQUEST_METHOD=GET; cgi-fcgi -bind -connect /var/run/php/php7.0-fpm.sock"
Content-type: text/plain;charset=UTF-8
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store, must-revalidate, max-age=0

imok

Use the following command to perform specific operation that depends on the received response.

$ sudo -u www-data bash -c "export SCRIPT_NAME=/ruok; export SCRIPT_FILENAME=/ruok; export REQUEST_METHOD=GET; cgi-fcgi -bind -connect /var/run/php/php7.0-fpm.sock" 2>/dev/null | tail -1 | awk '{if ($0 == "imok") {exit 0} else {exit 1}}' && (echo "green") || (echo "red")

It will display green or red status code, so you can easily modify it.

Use Python script to monitor FastCGI server using UNIX socket

Use the following Python script to monitor FastCGI server using UNIX socket without additional dependencies.

#!/usr/bin/env python 

import sys
import socket
import struct


class FCGIPingClient:
  # 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, challenge = "/ping", response = "pong" ):
    self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    self.socket_path = socket_path
    self.set_socket_timeout(socket_timeout)
    self.challenge = challenge
    self.response  = response
    self.request_id = 1

    self.params = {
      "SCRIPT_NAME": self.challenge,
      "SCRIPT_FILENAME": self.challenge,
      "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_data=self.socket.recv(request_length)
      else:
        self.raw_status_data = ""
        if request_type == 7:
          sys.exit(2)
        else:
          sys.exit(3)
    except:
      sys.exit(4)
    self.data = self.raw_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 return_status(self):
    if self.data == self.response:
      sys.exit(0)
    else:
      sys.exit(1)

fcgi_client = FCGIPingClient( socket_path = "/var/run/php/php7.0-fpm.sock", challenge = "/ruok", response = "imok" )
fcgi_client.make_request()
fcgi_client.return_status()

Use the following command to perform specific operation that depends on the received response.

$ sudo -u www-data python /usr/local/bin/fpm-monitor.py && (echo "green") || (echo "red") 

It it using the same mechanism as the previous one.

Milosz Galazka's Picture

About Milosz Galazka

Milosz is a Linux Foundation Certified Engineer working for a successful Polish company as a system administrator and a long time supporter of Free Software Foundation and Debian operating system.