This is an automated email from the ASF dual-hosted git repository.
nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new e14c6e9371 feat(prometheus): export per-process timers, connection
slots and Lua memory (#14034)
e14c6e9371 is described below
commit e14c6e9371edc084cc51321d4bee78646fa8367d
Author: Nic <[email protected]>
AuthorDate: Fri Oct 16 09:58:41 2026 +0800
feat(prometheus): export per-process timers, connection slots and Lua
memory (#14034)
---
apisix/cli/ngx_tpl.lua | 9 +
apisix/plugins/prometheus/exporter.lua | 32 +-
apisix/plugins/prometheus/process_metrics.lua | 442 ++++++++++++++++++++++++++
docs/en/latest/plugins/prometheus.md | 17 +
docs/zh/latest/plugins/prometheus.md | 17 +
t/cli/test_prometheus_reload.sh | 106 ++++++
t/cli/test_prometheus_stream.sh | 39 +++
t/plugin/prometheus-process-metrics.t | 210 ++++++++++++
8 files changed, 866 insertions(+), 6 deletions(-)
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index e7262a3541..d872ff3b56 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -90,6 +90,15 @@ http {
.. [=[$prefix/deps/lib/lua/5.1/?.so;;]=]
.. [=[{*lua_cpath*};";
+ # the same limits as the stream subsystem, which nginx_timers_max
+ # reports for every Lua VM
+ {% if max_pending_timers then %}
+ lua_max_pending_timers {* max_pending_timers *};
+ {% end %}
+ {% if max_running_timers then %}
+ lua_max_running_timers {* max_running_timers *};
+ {% end %}
+
{% if enabled_stream_plugins["prometheus"] then %}
init_by_lua_block {
diff --git a/apisix/plugins/prometheus/exporter.lua
b/apisix/plugins/prometheus/exporter.lua
index 7fe666c7a7..4ccc3f56b7 100644
--- a/apisix/plugins/prometheus/exporter.lua
+++ b/apisix/plugins/prometheus/exporter.lua
@@ -45,6 +45,7 @@ local get_protos =
require("apisix.plugins.grpc-transcode.proto").protos
local service_fetch = require("apisix.http.service").get
local latency_details = require("apisix.utils.log-util").latency_details_in_ms
local xrpc = require("apisix.stream.xrpc")
+local process_metrics = require("apisix.plugins.prometheus.process_metrics")
local unpack = unpack
local next = next
local str_sub = string.sub
@@ -500,12 +501,22 @@ local function collect_stream_zone_metrics()
end
+local function get_refresh_interval()
+ local attr = plugin.plugin_attr("prometheus")
+ if attr and attr.refresh_interval then
+ return attr.refresh_interval
+ end
+ return DEFAULT_REFRESH_INTERVAL
+end
+
+
function _M.http_init(prometheus_enabled_in_stream)
-- todo: support hot reload, we may need to update the lua-prometheus
-- library
if ngx.get_phase() ~= "init" and ngx.get_phase() ~= "init_worker" then
if prometheus_bkp then
prometheus = prometheus_bkp
+ process_metrics.start(get_refresh_interval())
end
return
end
@@ -695,6 +706,8 @@ function _M.http_init(prometheus_enabled_in_stream)
ai_cache_embedding_latency_buckets,
ai_cache_embedding_latency_exptime)
+ process_metrics.init(prometheus)
+
if prometheus_enabled_in_stream then
init_stream_metrics()
end
@@ -703,6 +716,7 @@ end
function _M.stream_init()
if ngx.get_phase() ~= "init" and ngx.get_phase() ~= "init_worker" then
+ process_metrics.start(get_refresh_interval())
return
end
@@ -722,6 +736,9 @@ function _M.stream_init()
prometheus = base_prometheus.init("prometheus-metrics", metric_prefix)
init_stream_metrics()
+
+ process_metrics.init(prometheus)
+ process_metrics.start(get_refresh_interval())
end
@@ -1186,6 +1203,9 @@ end
local function collect(yieldable)
+ -- the other processes report their own values on a timer
+ process_metrics.refresh()
+
-- collect ngx.shared.DICT status
shared_dict_status()
@@ -1291,16 +1311,14 @@ end
local function init_exporter_timer()
+ local refresh_interval = get_refresh_interval()
+
+ process_metrics.start(refresh_interval)
+
if process.type() ~= "privileged agent" then
return
end
- local refresh_interval = DEFAULT_REFRESH_INTERVAL
- local attr = plugin.plugin_attr("prometheus")
- if attr and attr.refresh_interval then
- refresh_interval = attr.refresh_interval
- end
-
local cache_exptime = refresh_interval * 2
exporter_timer(false, false, cache_exptime)
@@ -1457,6 +1475,8 @@ end
function _M.destroy()
+ process_metrics.stop()
+
if prometheus ~= nil then
prometheus_bkp = prometheus
prometheus = nil
diff --git a/apisix/plugins/prometheus/process_metrics.lua
b/apisix/plugins/prometheus/process_metrics.lua
new file mode 100644
index 0000000000..ff4ddfb4c3
--- /dev/null
+++ b/apisix/plugins/prometheus/process_metrics.lua
@@ -0,0 +1,442 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+-- Per-process OpenResty internals: Lua timers, connection slots and Lua VM
+-- memory. These values only exist inside the process they describe, while the
+-- exposition is rendered by a single process, so every process writes its own
+-- gauges into the shared metric dict from a timer, and the process rendering
+-- the exposition refreshes its own values right before it does.
+local core = require("apisix.core")
+local ffi = require("ffi")
+local process = require("ngx.process")
+local C = ffi.C
+local ngx = ngx
+local timer_at = ngx.timer.at
+local collectgarbage = collectgarbage
+local tonumber = tonumber
+local tostring = tostring
+local pcall = pcall
+local ipairs = ipairs
+local pairs = pairs
+local select = select
+
+
+-- The prefix of struct ngx_cycle_s up to write_events. Its layout is the same
+-- in every nginx core shipped by the supported runtimes (1.21.4 to 1.29.x);
+-- the self check in check_cycle_layout() is what keeps a future change from
+-- turning into a wrong number.
+ffi.cdef[[
+typedef struct {
+ uintptr_t log_level;
+ void *file;
+ uintptr_t connection;
+ long disk_full_time;
+ void *handler;
+ void *data;
+ void *writer;
+ void *wdata;
+ char *action;
+ void *next;
+} apisix_prom_ngx_log_t;
+
+typedef struct {
+ void *elts;
+ uintptr_t nelts;
+ size_t size;
+ uintptr_t nalloc;
+ void *pool;
+} apisix_prom_ngx_array_t;
+
+typedef struct {
+ void *elts;
+ uintptr_t nelts;
+ void *next;
+} apisix_prom_ngx_list_part_t;
+
+typedef struct {
+ void *last;
+ apisix_prom_ngx_list_part_t part;
+ size_t size;
+ uintptr_t nalloc;
+ void *pool;
+} apisix_prom_ngx_list_t;
+
+typedef struct {
+ uintptr_t key;
+ void *left;
+ void *right;
+ void *parent;
+ unsigned char color;
+ unsigned char data;
+} apisix_prom_ngx_rbtree_node_t;
+
+typedef struct {
+ void *root;
+ void *sentinel;
+ void *insert;
+} apisix_prom_ngx_rbtree_t;
+
+typedef struct {
+ void *data;
+ void *read;
+ void *write;
+} apisix_prom_ngx_connection_head_t;
+
+typedef struct {
+ void *conf_ctx;
+ void *pool;
+ void *log;
+ apisix_prom_ngx_log_t new_log;
+ uintptr_t log_use_stderr;
+ void *files;
+ void *free_connections;
+ uintptr_t free_connection_n;
+ void *modules;
+ uintptr_t modules_n;
+ uintptr_t modules_used;
+ void *reusable_connections_queue[2];
+ uintptr_t reusable_connections_n;
+ long connections_reuse_time;
+ apisix_prom_ngx_array_t listening;
+ apisix_prom_ngx_array_t paths;
+ apisix_prom_ngx_array_t config_dump;
+ apisix_prom_ngx_rbtree_t config_dump_rbtree;
+ apisix_prom_ngx_rbtree_node_t config_dump_sentinel;
+ apisix_prom_ngx_list_t open_files;
+ apisix_prom_ngx_list_t shared_memory;
+ uintptr_t connection_n;
+ uintptr_t files_n;
+ apisix_prom_ngx_connection_head_t *connections;
+ void *read_events;
+ void *write_events;
+} apisix_prom_ngx_cycle_t;
+
+extern apisix_prom_ngx_cycle_t *ngx_cycle;
+]]
+
+
+-- nginx's own defaults for lua_max_running_timers / lua_max_pending_timers,
+-- in effect when the configuration leaves them out
+local DEFAULT_MAX_RUNNING_TIMERS = 256
+local DEFAULT_MAX_PENDING_TIMERS = 1024
+local DEFAULT_INTERVAL = 15
+
+-- Shared by every process, so a layout mismatch found by one worker keeps the
+-- connection gauges out of the exposition everywhere, including the
+-- privileged agent, whose connection count cannot be checked against config.
+local CONNECTIONS_VERDICT_KEY = "process_metrics:connections_readable"
+
+local _M = {}
+
+local gauges
+local registry
+local dict
+local process_label
+local no_extra_labels = {}
+local extra_label_names = no_extra_labels
+local extra_label_values
+local subsystem = ngx.config.subsystem
+local verdict_logged = false
+
+local timer_running = false
+local stopped = true
+local interval = DEFAULT_INTERVAL
+local retired_cleanups = 0
+
+
+local function get_process_label()
+ if process.type() == "privileged agent" then
+ return "privileged-agent"
+ end
+
+ return "worker-" .. tostring(ngx.worker.id() or 0)
+end
+
+
+-- The CLI refuses to start without nginx_config, but a forced re-read of
+-- config.yaml at runtime (plugin reload) can still drop it: `nginx_config: ~`
+-- deletes the default table while merging.
+local function nginx_config()
+ return core.config.local_conf().nginx_config or {}
+end
+
+
+local function timers_max()
+ local conf = nginx_config()
+ return conf.max_running_timers or DEFAULT_MAX_RUNNING_TIMERS,
+ conf.max_pending_timers or DEFAULT_MAX_PENDING_TIMERS
+end
+
+
+local function read_cycle()
+ local cycle = C.ngx_cycle
+ local connection_n = tonumber(cycle.connection_n)
+ local free_n = tonumber(cycle.free_connection_n)
+
+ -- every connection slot is wired to its own pair of events at startup, so
+ -- this pins down the offsets right after connection_n as well
+ local conns = cycle.connections
+ if connection_n == 0 or free_n > connection_n or conns == nil
+ or conns[0].read ~= cycle.read_events or conns[0].write ~=
cycle.write_events
+ then
+ return nil
+ end
+
+ return connection_n, free_n
+end
+
+
+local function check_cycle_layout()
+ local ok, connection_n = pcall(read_cycle)
+ if not ok or not connection_n then
+ return false, "unexpected nginx cycle layout"
+ end
+
+ local event = 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
+
+ return true
+end
+
+
+-- Only a worker can tell whether the numbers read through FFI are right: its
+-- connection slots must match worker_connections from the configuration.
+local function connections_readable()
+ local readable = dict:get(CONNECTIONS_VERDICT_KEY)
+ if readable ~= nil then
+ return readable
+ end
+
+ if process.type() == "privileged agent" then
+ -- no worker has checked yet
+ return false
+ end
+
+ local ok, err = check_cycle_layout()
+ dict:set(CONNECTIONS_VERDICT_KEY, ok)
+ if not ok then
+ gauges.connections:reset()
+ gauges.connections_max:reset()
+ if not verdict_logged then
+ verdict_logged = true
+ core.log.warn("process connection metrics are disabled: ", err)
+ end
+ end
+
+ return ok
+end
+
+
+local function with_extra_labels(labels)
+ for _, name in ipairs(extra_label_names) do
+ labels[#labels + 1] = name
+ end
+ return labels
+end
+
+
+local function label_values(extra, ...)
+ local values = {...}
+ local n = select("#", ...)
+ for i, value in ipairs(extra) do
+ values[n + i] = value
+ end
+ return values
+end
+
+
+-- extra_labels, when given, appends the same labels to every gauge here:
+-- {names = {...}, values = function() return {...} end}
+function _M.init(prometheus_registry, extra_labels)
+ registry = prometheus_registry
+ dict = registry.dict
+ if extra_labels then
+ extra_label_names = extra_labels.names
+ extra_label_values = extra_labels.values
+ end
+
+ gauges = {
+ timers = registry:gauge("nginx_timers",
+ "Number of Lua timers per process and subsystem, by state",
+ with_extra_labels({"process", "subsystem", "state"})),
+ timers_max = registry:gauge("nginx_timers_max",
+ "Configured maximum number of Lua timers per process, by state",
+ with_extra_labels({"state"})),
+ connections = registry:gauge("nginx_process_connections",
+ "Number of connection slots in use per nginx process",
+ with_extra_labels({"process"})),
+ connections_max = registry:gauge("nginx_process_connections_max",
+ "Number of connection slots per nginx process",
+ with_extra_labels({"process"})),
+ lua_memory = registry:gauge("nginx_lua_memory_bytes",
+ "Memory allocated by the Lua VM per process and subsystem",
+ with_extra_labels({"process", "subsystem"})),
+ }
+
+ -- the verdict describes the binary and the configuration of the processes
+ -- starting now, so each worker checks again rather than trusting the one
+ -- left behind in the shared dict by a reload
+ if subsystem == "http" and process.type() ~= "privileged agent" then
+ dict:delete(CONNECTIONS_VERDICT_KEY)
+ end
+end
+
+
+local function report()
+ process_label = process_label or get_process_label()
+ local extra = extra_label_values and extra_label_values() or
no_extra_labels
+
+ local timers = gauges.timers
+ timers:set(ngx.timer.running_count(),
+ label_values(extra, process_label, subsystem, "running"))
+ timers:set(ngx.timer.pending_count(),
+ label_values(extra, process_label, subsystem, "pending"))
+
+ gauges.lua_memory:set(collectgarbage("count") * 1024,
+ label_values(extra, process_label, subsystem))
+
+ -- the remaining values are per process rather than per Lua VM, so only
+ -- the http VM reports them; it is present in every process that exports
+ if subsystem ~= "http" then
+ return
+ end
+
+ local max_running, max_pending = timers_max()
+ gauges.timers_max:set(max_running, label_values(extra, "running"))
+ gauges.timers_max:set(max_pending, label_values(extra, "pending"))
+
+ if not connections_readable() then
+ return
+ end
+
+ local connection_n, free_n = read_cycle()
+ if not connection_n then
+ return
+ end
+
+ gauges.connections:set(connection_n - free_n, label_values(extra,
process_label))
+ gauges.connections_max:set(connection_n, label_values(extra,
process_label))
+end
+
+
+-- Writes this process's values now. A failure here is logged rather than
+-- raised, so it can never cost the caller the rest of the exposition.
+function _M.refresh()
+ if not gauges then
+ return
+ end
+
+ local ok, err = pcall(report)
+ if not ok then
+ core.log.error("failed to report process metrics: ", err)
+ end
+end
+
+
+-- A reload with fewer workers leaves the series of the workers that are gone
+-- in the shared dict, where nothing overwrites them any more. Expiring them is
+-- not an option: some nginx-lua-prometheus versions do not renew the key index
+-- of a series that is set again, so a live series would expire as well.
+local function remove_retired_workers()
+ local names = {}
+ for _, gauge in pairs(gauges) do
+ names[gauge.name] = true
+ end
+
+ local worker_count = ngx.worker.count()
+ local key_index = registry.key_index
+ for _, key in ipairs(key_index:list()) do
+ local name = key:match("^([^{]+){")
+ local id = name and names[name] and
key:match('process="worker%-(%d+)"')
+ if id and tonumber(id) >= worker_count then
+ key_index:remove(key)
+ dict:delete(key)
+ end
+ end
+end
+
+
+local function tick(premature)
+ if premature or stopped then
+ timer_running = false
+ if stopped then
+ core.log.info("process metrics stopped")
+ end
+ return
+ end
+
+ if retired_cleanups > 0 then
+ retired_cleanups = retired_cleanups - 1
+ local ok, err = pcall(remove_retired_workers)
+ if not ok then
+ core.log.error("failed to remove the metrics of retired workers:
", err)
+ end
+ end
+
+ _M.refresh()
+
+ local ok, err = timer_at(interval, tick)
+ if not ok then
+ timer_running = false
+ core.log.error("failed to schedule process metrics: ", err)
+ end
+end
+
+
+-- Idempotent: calling it again while reporting only updates the interval.
+function _M.start(report_interval)
+ if not gauges then
+ return
+ end
+
+ interval = report_interval or DEFAULT_INTERVAL
+ stopped = false
+
+ -- one process is enough, and the privileged agent always exists. The
second
+ -- pass, one interval later, catches a sample that an exiting worker was
+ -- still writing while the first one ran.
+ if subsystem == "http" and process.type() == "privileged agent" then
+ retired_cleanups = 2
+ end
+
+ 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)
+ return
+ end
+
+ core.log.info("process metrics started, reporting every ", interval, "s")
+end
+
+
+-- The pending timer sees this when it fires and does not schedule another.
+function _M.stop()
+ stopped = true
+end
+
+
+return _M
diff --git a/docs/en/latest/plugins/prometheus.md
b/docs/en/latest/plugins/prometheus.md
index e0f16dad9e..b793cf0709 100644
--- a/docs/en/latest/plugins/prometheus.md
+++ b/docs/en/latest/plugins/prometheus.md
@@ -170,6 +170,11 @@ The following table lists the core metrics registered
directly by the `prometheu
| apisix_node_info | gauge | Information of the
APISIX node, such as host name and the current APISIX version.
|
| apisix_shared_dict_capacity_bytes | gauge | The total capacity of an
[NGINX shared
dictionary](https://github.com/openresty/lua-nginx-module#ngxshareddict).
|
| apisix_shared_dict_free_space_bytes | gauge | The remaining space in
an [NGINX shared
dictionary](https://github.com/openresty/lua-nginx-module#ngxshareddict).
|
+| apisix_nginx_timers | gauge | Number of running and
pending Lua timers of each NGINX process, per subsystem.
|
+| apisix_nginx_timers_max | gauge | Configured maximum
number of running and pending Lua timers per process
(`nginx_config.max_running_timers` and `nginx_config.max_pending_timers`).
|
+| apisix_nginx_process_connections | gauge | Number of connection
slots in use by each NGINX process, including listening sockets, client
connections and outgoing cosocket connections.
|
+| apisix_nginx_process_connections_max | gauge | Number of connection
slots of each NGINX process: `worker_connections` for workers, and 512 for the
privileged agent.
|
+| apisix_nginx_lua_memory_bytes | gauge | Memory allocated by the
Lua VM of each NGINX process, per subsystem.
|
| apisix_upstream_status | gauge | Health check status of
upstream nodes, available if health checks are configured on the upstream. A
value of `1` represents healthy and `0` represents unhealthy.
|
| apisix_stream_connection_total | counter | Total number of
connections handled per Stream Route.
|
| apisix_stream_active_connections | gauge | Number of Stream
sessions currently being proxied, per listening address. Covers TCP connections
and UDP sessions alike. |
@@ -440,6 +445,18 @@ The following labels are used to differentiate
`apisix_upstream_status` metrics.
| ip | IP address of the upstream node.
|
| port | Port number of the node.
|
+### Labels for the NGINX process metrics
+
+`apisix_nginx_timers`, `apisix_nginx_timers_max`,
`apisix_nginx_process_connections`, `apisix_nginx_process_connections_max` and
`apisix_nginx_lua_memory_bytes` describe the internals of each NGINX process,
so a resource running out in a single process, such as the privileged agent,
shows up before it causes errors. Every process writes its own values every
`refresh_interval`, and the process building the exposition refreshes its own
values first.
+
+| Name | Description
|
+| --------- |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
+| process | The process the value belongs to: `worker-<id>` for a worker
process, or `privileged-agent`.
|
+| subsystem | `http` or `stream`. Each subsystem has a Lua VM of its own, with
its own timers and memory. Only on `apisix_nginx_timers` and
`apisix_nginx_lua_memory_bytes`. |
+| state | `running` or `pending`. Only on `apisix_nginx_timers` and
`apisix_nginx_timers_max`.
|
+
+The connection metrics are read from NGINX's internal cycle structure. When a
worker finds that the number of connection slots it reads does not match
`worker_connections` in the configuration, APISIX logs a warning and leaves
both connection metrics out for every process instead of exporting wrong
numbers.
+
## Examples
The examples below demonstrate how you can work with the `prometheus` Plugin
for different scenarios.
diff --git a/docs/zh/latest/plugins/prometheus.md
b/docs/zh/latest/plugins/prometheus.md
index 13128a44ac..33b513950f 100644
--- a/docs/zh/latest/plugins/prometheus.md
+++ b/docs/zh/latest/plugins/prometheus.md
@@ -170,6 +170,11 @@ Prometheus 中有不同类型的指标。要了解它们之间的区别,请参
| apisix_node_info | gauge | APISIX 节点的信息,例如主机名和当前的 APISIX 版本号。
|
| apisix_shared_dict_capacity_bytes | gauge | [NGINX
共享字典](https://github.com/openresty/lua-nginx-module#ngxshareddict) 的总容量。
|
| apisix_shared_dict_free_space_bytes | gauge | [NGINX
共享字典](https://github.com/openresty/lua-nginx-module#ngxshareddict) 中剩余的空间。
|
+| apisix_nginx_timers | gauge | 每个 NGINX 进程中运行中和等待中的 Lua
定时器数量,按子系统区分。
|
+| apisix_nginx_timers_max | gauge | 每个进程允许的运行中和等待中 Lua
定时器的上限(`nginx_config.max_running_timers` 和 `nginx_config.max_pending_timers`)。
|
+| apisix_nginx_process_connections | gauge | 每个 NGINX
进程已占用的连接槽位数,包括监听套接字、客户端连接和 cosocket 发起的出站连接。
|
+| apisix_nginx_process_connections_max | gauge | 每个 NGINX 进程的连接槽位总数:worker 进程为
`worker_connections`,privileged agent 为 512。
|
+| apisix_nginx_lua_memory_bytes | gauge | 每个 NGINX 进程中 Lua VM 分配的内存,按子系统区分。
|
| apisix_upstream_status | gauge | 上游节点的健康检查状态,如果在上游配置了健康检查,则可用。值为 `1`
表示健康,`0` 表示不健康。
|
| apisix_stream_connection_total | counter | 每个 Stream Route 处理的总连接数。
|
| apisix_stream_active_connections | gauge | 当前正在代理的 Stream 会话数,按监听地址区分。同时覆盖
TCP 连接与 UDP 会话。
|
@@ -428,6 +433,18 @@ APISIX 延迟
| ip | 上游节点的 IP 地址。
|
| port | 节点的端口号。
|
+### NGINX 进程指标的标签
+
+`apisix_nginx_timers`、`apisix_nginx_timers_max`、`apisix_nginx_process_connections`、`apisix_nginx_process_connections_max`
和 `apisix_nginx_lua_memory_bytes` 描述每个 NGINX 进程的内部状态,因此单个进程(例如 privileged
agent)的资源即将耗尽时,可以在它引发错误之前被发现。每个进程每隔 `refresh_interval`
写入一次自己的数值,生成指标输出的进程会先刷新自己的数值。
+
+| 名称 | 描述
|
+| --------- |
----------------------------------------------------------------------------------------------------------------------------
|
+| process | 数值所属的进程:worker 进程为 `worker-<id>`,privileged agent 为
`privileged-agent`。 |
+| subsystem | `http` 或 `stream`。每个子系统有独立的 Lua VM,定时器和内存也各自独立。仅用于
`apisix_nginx_timers` 和 `apisix_nginx_lua_memory_bytes`。 |
+| state | `running` 或 `pending`。仅用于 `apisix_nginx_timers` 和
`apisix_nginx_timers_max`。 |
+
+连接相关指标读取自 NGINX 内部的 cycle 结构。如果某个 worker 读到的连接槽位数与配置中的 `worker_connections`
不一致,APISIX 会记录一条警告,并在所有进程中都不导出这两个连接指标,而不是导出错误的数值。
+
## 示例
以下示例演示如何在不同场景中使用 `prometheus` 插件。
diff --git a/t/cli/test_prometheus_reload.sh b/t/cli/test_prometheus_reload.sh
index 7a8b1a1f81..f4ee9f9e45 100755
--- a/t/cli/test_prometheus_reload.sh
+++ b/t/cli/test_prometheus_reload.sh
@@ -89,3 +89,109 @@ if curl -i http://127.0.0.1:9091/apisix/prometheus/metrics
| grep "{}" > /dev/nu
echo "failed: metrics should not contain '{}' when prometheus is enabled"
exit 1
fi
+
+# The per-process metrics follow the plugin through hot reloads: each process
+# runs exactly one reporting timer while prometheus is enabled and none while
+# it is disabled.
+make stop
+sleep 0.5
+rm logs/error.log || true
+
+write_conf() {
+ echo "
+deployment:
+ role: traditional
+ role_traditional:
+ config_provider: etcd
+ admin:
+ admin_key: null
+apisix:
+ node_listen: 1984
+nginx_config:
+ error_log_level: info
+ worker_processes: $1
+plugins:
+ - $2
+plugin_attr:
+ prometheus:
+ refresh_interval: 1
+" > conf/config.yaml
+}
+
+# wait_for <description> <command...>: polls for up to 20 seconds
+wait_for() {
+ local desc=$1
+ shift
+ local deadline=$(( $(date +%s) + 20 ))
+ { set +x; } 2>/dev/null
+ while [ "$(date +%s)" -lt "$deadline" ]; do
+ if "$@"; then
+ set -x
+ return 0
+ fi
+ sleep 0.5
+ done
+ set -x
+ echo "failed: $desc"
+ exit 1
+}
+
+metrics() {
+ curl -s --connect-timeout 1 --max-time 2
http://127.0.0.1:9091/apisix/prometheus/metrics || true
+}
+
+# 2 workers and the privileged agent, http only
+started_is() { [ "$(grep -c 'process metrics started' logs/error.log)" -eq
"$1" ]; }
+stopped_is() { [ "$(grep -c 'process metrics stopped' logs/error.log)" -eq
"$1" ]; }
+reports_workers() {
+ local out
+ out="$(metrics)"
+ echo "$out" | grep -qE
'apisix_nginx_lua_memory_bytes\{process="worker-0",subsystem="http"\} [0-9]' \
+ && echo "$out" | grep -qE
'apisix_nginx_lua_memory_bytes\{process="worker-1",subsystem="http"\} [0-9]' \
+ && echo "$out" | grep -qE
'apisix_nginx_lua_memory_bytes\{process="privileged-agent",subsystem="http"\}
[0-9]'
+}
+exports_nothing() { metrics | grep -q "{}"; }
+
+write_conf 2 prometheus
+make run
+
+wait_for "every process reports when prometheus is enabled" reports_workers
+wait_for "one reporting timer per process" started_is 3
+
+write_conf 2 ip-restriction
+curl -s -o /dev/null http://127.0.0.1:9090/v1/plugins/reload -XPUT
+
+wait_for "every reporting timer stops when prometheus is disabled" stopped_is 3
+wait_for "nothing is exported when prometheus is disabled" exports_nothing
+
+write_conf 2 prometheus
+curl -s -o /dev/null http://127.0.0.1:9090/v1/plugins/reload -XPUT
+
+wait_for "reporting resumes when prometheus is enabled again" started_is 6
+wait_for "every process reports again" reports_workers
+
+# reloading with prometheus still enabled keeps the running timer
+curl -s -o /dev/null http://127.0.0.1:9090/v1/plugins/reload -XPUT
+sleep 3
+if ! started_is 6 || ! stopped_is 3; then
+ echo "failed: a plugin reload must not start a second reporting timer"
+ exit 1
+fi
+
+echo "passed: process metrics follow the plugin through hot reloads"
+
+# Scaling down must not leave the series of the workers that are gone.
+write_conf 1 prometheus
+make reload
+
+no_retired_worker() {
+ local out
+ out="$(metrics)"
+ echo "$out" | grep -qE
'apisix_nginx_lua_memory_bytes\{process="worker-0",subsystem="http"\} [0-9]' \
+ && ! echo "$out" | grep -q 'process="worker-1"'
+}
+wait_for "the series of a retired worker are removed" no_retired_worker
+
+make stop
+
+echo "passed: the series of retired workers are removed"
diff --git a/t/cli/test_prometheus_stream.sh b/t/cli/test_prometheus_stream.sh
index fcbfb5a2ee..c9b59f7a9c 100755
--- a/t/cli/test_prometheus_stream.sh
+++ b/t/cli/test_prometheus_stream.sh
@@ -124,3 +124,42 @@ if ! echo "$out" | grep "apisix_node_info{hostname=" >
/dev/null; then
fi
echo "passed: prometheus works when only stream is enabled"
+
+# the http subsystem exists only to export metrics here, yet every process
+# still reports its own Lua VMs and connection slots
+ok=0
+deadline=$(( $(date +%s) + 20 ))
+{ 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"\}
[0-9]+$' \
+ && echo "$out" | grep -qE
'apisix_nginx_lua_memory_bytes\{process="privileged-agent",subsystem="http"\}
[1-9]' \
+ && echo "$out" | grep -qE
'apisix_nginx_process_connections_max\{process="privileged-agent"\} 512$'; then
+ ok=1
+ break
+ fi
+ sleep 0.5
+done
+set -x
+if [ "$ok" -ne 1 ]; then
+ echo "failed: process metrics are missing when only stream is enabled"
+ exit 1
+fi
+
+echo "passed: process metrics work when only stream is enabled"
+
+# the export-only http block runs on the same timer limits as the stream
+# subsystem, so the reported limits hold for every Lua VM
+http_block="$(sed -n '/^http {/,/^}/p' conf/nginx.conf)"
+if ! echo "$http_block" | grep -q "lua_max_pending_timers 16384;" \
+ || ! echo "$http_block" | grep -q "lua_max_running_timers 4096;"; then
+ echo "failed: the stream-only http block does not carry the timer limits"
+ exit 1
+fi
+
+if ! echo "$out" | grep -qE 'apisix_nginx_timers_max\{state="pending"\}
16384$'; then
+ echo "failed: nginx_timers_max does not report the configured limit"
+ exit 1
+fi
+
+echo "passed: the stream-only http block carries the timer limits"
diff --git a/t/plugin/prometheus-process-metrics.t
b/t/plugin/prometheus-process-metrics.t
new file mode 100644
index 0000000000..059343fb00
--- /dev/null
+++ b/t/plugin/prometheus-process-metrics.t
@@ -0,0 +1,210 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!defined $block->request && !defined $block->stream_request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ # t::APISIX renders worker_connections 128 into nginx.conf, so the
+ # configuration has to say the same for the connection gauges to pass
+ # their self check
+ if (!defined $block->extra_yaml_config) {
+ $block->set_value("extra_yaml_config", <<_EOC_);
+stream_plugins:
+ - prometheus
+nginx_config:
+ max_running_timers: 1234
+ max_pending_timers: 5678
+ event:
+ worker_connections: 128
+plugin_attr:
+ prometheus:
+ refresh_interval: 0.1
+_EOC_
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: pre-create public API route
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/metrics',
+ ngx.HTTP_PUT,
+ [[{
+ "plugins": {
+ "public-api": {}
+ },
+ "uri": "/apisix/prometheus/metrics"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 2: every process reports its timers, connection slots and Lua memory
+--- config
+ location /t {
+ content_by_lua_block {
+ ngx.sleep(0.5)
+ local http = require("resty.http")
+ local httpc = http.new()
+ 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"\}
\d+\n]],
+
[[apisix_nginx_timers\{process="worker-0",subsystem="http",state="pending"\}
\d+]],
+
[[apisix_nginx_timers\{process="privileged-agent",subsystem="http",state="running"\}
\d+]],
+ [[apisix_nginx_timers_max\{state="running"\} 1234\n]],
+ [[apisix_nginx_timers_max\{state="pending"\} 5678\n]],
+ [[apisix_nginx_process_connections\{process="worker-0"\}
[1-9]\d*\n]],
+
[[apisix_nginx_process_connections\{process="privileged-agent"\} [1-9]\d*\n]],
+ [[apisix_nginx_process_connections_max\{process="worker-0"\}
128\n]],
+
[[apisix_nginx_process_connections_max\{process="privileged-agent"\} 512\n]],
+
[[apisix_nginx_lua_memory_bytes\{process="worker-0",subsystem="http"\}
[1-9]\d*]],
+
[[apisix_nginx_lua_memory_bytes\{process="privileged-agent",subsystem="http"\}
[1-9]\d*]],
+ }
+ for _, p in ipairs(patterns) do
+ if not ngx.re.find(res.body, p, "jo") then
+ ngx.say("missing ", p)
+ end
+ end
+ ngx.say("done")
+ }
+ }
+--- response_body
+done
+
+
+
+=== TEST 3: connection slots held by cosockets show up in the worker's gauge
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require("resty.http")
+
+ local function worker_connections()
+ ngx.sleep(0.5)
+ local httpc = http.new()
+ local res = assert(httpc:request_uri(
+ "http://127.0.0.1:1984/apisix/prometheus/metrics"))
+ local m = ngx.re.match(res.body,
+ [[apisix_nginx_process_connections\{process="worker-0"\}
(\d+)]], "jo")
+ return tonumber(m[1])
+ end
+
+ local before = worker_connections()
+
+ local socks = {}
+ for i = 1, 20 do
+ local sock = ngx.socket.tcp()
+ assert(sock:connect("127.0.0.1", 1980))
+ socks[i] = sock
+ end
+
+ local held = worker_connections()
+
+ for _, sock in ipairs(socks) do
+ sock:close()
+ end
+
+ local released = worker_connections()
+
+ ngx.say("held: ", held - before >= 20)
+ ngx.say("released: ", held - released >= 20)
+ }
+ }
+--- response_body
+held: true
+released: true
+
+
+
+=== TEST 4: connection gauges stay out when the slots do not match
worker_connections
+--- extra_yaml_config
+plugin_attr:
+ prometheus:
+ refresh_interval: 0.1
+--- config
+ location /t {
+ content_by_lua_block {
+ ngx.sleep(0.5)
+ local http = require("resty.http")
+ local httpc = http.new()
+ local res = assert(httpc:request_uri(
+ "http://127.0.0.1:1984/apisix/prometheus/metrics"))
+ ngx.say(ngx.re.find(res.body, "apisix_nginx_process_connections",
"jo") == nil)
+ ngx.say(ngx.re.find(res.body,
+
[[apisix_nginx_timers\{process="worker-0",subsystem="http",state="pending"\}]],
+ "jo") ~= nil)
+ }
+ }
+--- response_body
+true
+true
+--- error_log
+process connection metrics are disabled: worker connection slots read as 128,
but worker_connections is 10620
+
+
+
+=== TEST 5: the stream Lua VM of every process reports as well
+--- stream_server_config
+ content_by_lua_block {
+ ngx.req.socket():receive()
+ ngx.sleep(0.5)
+ local http = require("resty.http")
+ local httpc = http.new()
+ 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"\}
\d+\n]],
+
[[apisix_nginx_timers\{process="worker-0",subsystem="stream",state="pending"\}
\d+]],
+
[[apisix_nginx_timers\{process="privileged-agent",subsystem="stream",state="pending"\}
\d+]],
+
[[apisix_nginx_lua_memory_bytes\{process="worker-0",subsystem="stream"\}
[1-9]\d*]],
+
[[apisix_nginx_lua_memory_bytes\{process="privileged-agent",subsystem="stream"\}
[1-9]\d*]],
+ }
+ for _, p in ipairs(patterns) do
+ if not ngx.re.find(res.body, p, "jo") then
+ ngx.say("missing ", p)
+ end
+ end
+ ngx.say("done")
+ }
+--- stream_request
+ping
+--- stream_response
+done