Skip to content

feat(prometheus): export per-process timers, connection slots and Lua memory - #14034

Open
nic-6443 wants to merge 3 commits into
apache:masterfrom
nic-6443:feat/prometheus-openresty-internals
Open

nic-6443 wants to merge 3 commits into
apache:masterfrom
nic-6443:feat/prometheus-openresty-internals

Conversation

@nic-6443

Copy link
Copy Markdown
Member

Description

This adds Prometheus gauges for a few OpenResty internals that can run out per process, so exhaustion becomes visible before it turns into errors:

  • apisix_nginx_timers{process, subsystem, state}: ngx.timer.running_count() / pending_count() of each Lua VM
  • apisix_nginx_timers_max{state}: the configured max_running_timers / max_pending_timers
  • apisix_nginx_process_connections{process}: connection slots in use in each process (connection_n - free_connection_n)
  • apisix_nginx_process_connections_max{process}: connection slots of each process (worker_connections for workers, 512 for the privileged agent)
  • apisix_nginx_lua_memory_bytes{process, subsystem}: collectgarbage("count") of each Lua VM

process is worker-<id> or privileged-agent. The motivating case is a privileged agent that slowly leaks cosockets until its 512 connection slots are gone: the workers look perfectly healthy, and nginx_http_current_connections (stub_status) only counts accepted client connections, so nothing in the current exposition moves until the agent starts failing.

None of these values can be read from another process, so each process writes its own gauges into the metric dict on a timer (every refresh_interval), and the privileged agent refreshes its own values right before it renders the cache. The collection lives in its own module, apisix/plugins/prometheus/process_metrics.lua; the exporter only registers it and starts/stops it together with the plugin, so nothing new runs when prometheus is disabled. The http and the stream VM report separately (they have their own timers and memory), and the per-process values (connections, timer limits) come from the http VM only.

The connection numbers are read through FFI from the exported ngx_cycle. The struct prefix used is identical in every nginx core shipped in recent runtimes, but a layout change would silently produce garbage, so there are two guards: every process checks that the first connection slot is wired to the first read/write events, and a worker checks that the number of slots it reads equals worker_connections from the config. If that check fails, it logs one warning and the two connection gauges are dropped for every process, including the privileged agent, instead of exporting a wrong number.

Tests cover all five gauges for the worker and the privileged agent in both subsystems, a worker's connection gauge going up by the number of cosockets held open and back down when they are closed, the mismatch case, and the stream-only mode in t/cli/test_prometheus_stream.sh. Docs are updated in both languages.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible

Copilot AI balanced review requested due to automatic review settings October 14, 2026 08:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

6 open findings
What changed in this PR

Adds Prometheus process-scoped metrics for OpenResty internals (timers, connection slots, Lua VM memory) so per-process resource exhaustion (notably privileged-agent) becomes visible before failures.

Changes:

  • Introduces a new process_metrics module to collect/report per-process gauges via shared metric dict.
  • Hooks process-metric collection into the Prometheus exporter lifecycle (init/start/stop/refresh).
  • Adds/extends tests and documentation (EN/ZH) to cover/explain the new gauges (including stream-only mode).
File Description
t/​plugin/​prometheus-process-metrics.t Adds integration tests for the new per-process timers/connections/memory gauges (http + stream, worker + privileged-agent).
t/​cli/​test_prometheus_stream.sh Extends stream-only CI test to assert new process metrics are present.
docs/​zh/​latest/​plugins/​prometheus.md Documents the new metrics and label semantics in Chinese.
docs/​en/​latest/​plugins/​prometheus.md Documents the new metrics and label semantics in English.
apisix/​plugins/​prometheus/​process_metrics.lua New collector module that periodically writes per-process gauges (timers, connection slots, Lua memory) into the metrics dict.
apisix/​plugins/​prometheus/​exporter.lua Integrates process_metrics init/start/stop and refreshes local process values during collection.

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +198 to +203
local event = core.config.local_conf().nginx_config.event
local expected = event and tonumber(event.worker_connections)
if connection_n ~= expected then
return false, "worker connection slots read as " .. tostring(connection_n)
.. ", but worker_connections is " .. tostring(expected)
end
{ set +x; } 2>/dev/null
while [ "$(date +%s)" -lt "$deadline" ]; do
out="$(curl -s --connect-timeout 1 --max-time 2 http://127.0.0.1:9091/apisix/prometheus/metrics || true)"
if echo "$out" | grep -qE 'apisix_nginx_timers\{process="worker-0",subsystem="stream",state="running"\} [1-9]' \
local res = assert(httpc:request_uri(
"http://127.0.0.1:1984/apisix/prometheus/metrics"))
local patterns = {
[[apisix_nginx_timers\{process="worker-0",subsystem="http",state="running"\} [1-9]\d*]],
local res = assert(httpc:request_uri(
"http://127.0.0.1:1984/apisix/prometheus/metrics"))
local patterns = {
[[apisix_nginx_timers\{process="worker-0",subsystem="stream",state="running"\} [1-9]\d*]],
Comment on lines 1205 to +1208
local function collect(yieldable)
-- the other processes report their own values on a timer
process_metrics.refresh()

Comment on lines +359 to +382
function _M.start(report_interval)
if not gauges then
return
end

interval = report_interval or DEFAULT_INTERVAL
stopped = false
if timer_running then
return
end

timer_running = true
local ok, err = timer_at(0, tick)
if not ok then
timer_running = false
core.log.error("failed to start process metrics: ", err)
end
end


-- The pending timer sees this when it fires and does not schedule another.
function _M.stop()
stopped = true
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants