Skip to content

Instantly share code, notes, and snippets.

@lcrilly
Last active March 31, 2024 13:09
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save lcrilly/ad516de8378dd8ae5303a942e67d55b5 to your computer and use it in GitHub Desktop.
Save lcrilly/ad516de8378dd8ae5303a942e67d55b5 to your computer and use it in GitHub Desktop.
Prometheus exporter for NGINX Unit metrics

Prometheus exporter for NGINX Unit metrics

The NGINX Unit control API includes a /status endpoint for usage statistics. This is a solution for exposing these metrics in Prometheus format.

The application (run by Unit) queries the /status URI on the control socket and converts the JSON response into Prometheus text format. PHP and Python implementations are available.

These instructions assume an existing Unit installation with a working configuration. We will add a new listener on the default prometheus port (9090) and route it directly to the Prometheus app.

Step 0. Install the preferred Unit language module (unit-php or unit-python)

Step 1. Create a new group that has access to read and write to the Unit control socket (run these commands as root)

groupadd unitsock
chgrp unitsock /var/run/control.unit.sock
chmod g+rw /var/run/control.unit.sock

Note: Ownership and permissions of the control socket will be reset on restart/reboot.

Step 2a. Copy your preferred exporter implementation (prometheus.php or prometheus.py) to a suitable location on your Unit instance.

Step 2b. Create a JSON object as prom.json for your preferred exporter implementation, paying special attention to the location of the control socket.

{
    "type": "php",
    "root": "/path/to/app",
    "script": "prometheus.php",
    "group": "unitsock",
    "environment": {
    	"control_socket": "/var/run/control.unit.sock"
    }
}
{
    "type": "python",
    "path": "/path/to/app",
    "module": "prometheus",
    "group": "unitsock",
    "environment": {
    	"control_socket": "/var/run/control.unit.sock"
    }
}

Step 2b. Create the prom application

unitc /config/applications/prom < prom.json

Step 3. Create the listener and route it directly to the application

echo '{"pass": "applications/prom"}' | unitc /config/listeners/\*:9090

Step 4. Test the Prometheus endpoint

$ curl http://localhost:9090/
unit_connections_accepted_total 38
unit_connections_active 1
unit_connections_idle 0
unit_connections_closed_total 37
unit_requests_total 38
unit_application_prom_processes_running 1
unit_application_prom_processes_starting 0
unit_application_prom_processes_idle 0
unit_application_prom_requests_active 1
<?php
$sock = stream_socket_client("unix://".getenv("control_socket"), $errno, $errst);
fwrite($sock, "GET /status HTTP/1.0\r\n\r\n");
$resp = fread($sock, 4096);
fclose($sock);
list($headers, $body) = explode("\r\n\r\n", $resp, 2);
$json = json_decode($body);
$metrics = array();
array_push($metrics, "unit_connections_accepted_total ".$json->connections->accepted);
array_push($metrics, "unit_connections_active ".$json->connections->active);
array_push($metrics, "unit_connections_idle ".$json->connections->idle);
array_push($metrics, "unit_connections_closed_total ".$json->connections->closed);
array_push($metrics, "unit_requests_total ".$json->requests->total);
foreach($json->applications as $application => $data) {
array_push($metrics, "unit_application_".$application."_processes_running ".$data->processes->running);
array_push($metrics, "unit_application_".$application."_processes_starting ".$data->processes->starting);
array_push($metrics, "unit_application_".$application."_processes_idle ".$data->processes->idle);
array_push($metrics, "unit_application_".$application."_requests_active ".$data->requests->active);
}
header("Content-Type: text/plain");
echo join("\n", $metrics)."\n";
?>
import os
import socket
import json
def application(environ, start_response):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# Connect to the Unit control socket
try:
sock.connect(os.environ["control_socket"])
except socket.error as msg:
print(msg)
else:
sock.sendall(bytes("GET /status HTTP/1.0\r\n\r\n", "utf-8"))
response = sock.recv(4096)
sock.close()
headers, body = response.split(bytes("\r\n\r\n", "utf-8"), 1)
statj = json.loads(body)
metrics = []
metrics.append("unit_connections_accepted_total {}".format(statj["connections"]["accepted"]))
metrics.append("unit_connections_active {}".format(statj["connections"]["active"]))
metrics.append("unit_connections_idle {}".format(statj["connections"]["idle"]))
metrics.append("unit_connections_closed_total {}".format(statj["connections"]["closed"]))
metrics.append("unit_requests_total {}".format(statj["requests"]["total"]))
for application in statj["applications"].keys():
metrics.append("unit_application_" + application + "_processes_running {}".format(statj["applications"][application]["processes"]["running"]))
metrics.append("unit_application_" + application + "_processes_starting {}".format(statj["applications"][application]["processes"]["starting"]))
metrics.append("unit_application_" + application + "_processes_idle {}".format(statj["applications"][application]["processes"]["idle"]))
metrics.append("unit_application_" + application + "_requests_active {}".format(statj["applications"][application]["requests"]["active"]))
start_response("200 OK", [("Content-Type", "text/plain")])
yield str.encode("\n".join(metrics) + "\n")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment