You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by al...@apache.org on 2026/10/08 05:58:18 UTC
(apisix) branch master updated: feat(prometheus): add service and service_id to the stream metrics (#13992)
This is an automated email from the ASF dual-hosted git repository.
AlinsRan 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 2ee8dfa81 feat(prometheus): add service and service_id to the stream metrics (#13992)
2ee8dfa81 is described below
commit 2ee8dfa810b03da1972b88e024f46c61b1e1716e
Author: AlinsRan <al...@apache.org>
AuthorDate: Thu Oct 8 13:58:11 2026 +0800
feat(prometheus): add service and service_id to the stream metrics (#13992)
---
.requirements | 2 +-
apisix/plugins/prometheus/exporter.lua | 117 +++++-
apisix/stream/plugins/prometheus.lua | 1 +
ci/linux-install-openresty.sh | 6 +-
docs/en/latest/plugins/prometheus.md | 43 +-
docs/zh/latest/plugins/prometheus.md | 40 +-
t/cli/test_prometheus_stream.sh | 4 +-
t/stream-plugin/prometheus-metrics-service.t | 580 +++++++++++++++++++++++++++
t/stream-plugin/prometheus-metrics.t | 212 +++++++++-
t/stream-plugin/prometheus.t | 4 +-
10 files changed, 959 insertions(+), 50 deletions(-)
diff --git a/.requirements b/.requirements
index f633665c6..a26f9999d 100644
--- a/.requirements
+++ b/.requirements
@@ -17,5 +17,5 @@
APISIX_PACKAGE_NAME=apisix
-APISIX_RUNTIME=1.3.18
+APISIX_RUNTIME=1.3.19
APISIX_DASHBOARD_COMMIT=fa2fd0f60f8afffb096476333b9ba63b4c518fa3
diff --git a/apisix/plugins/prometheus/exporter.lua b/apisix/plugins/prometheus/exporter.lua
index fbb600e18..7fe666c7a 100644
--- a/apisix/plugins/prometheus/exporter.lua
+++ b/apisix/plugins/prometheus/exporter.lua
@@ -238,25 +238,29 @@ local _M = {
local function init_stream_metrics()
+ -- service and service_id follow the http metrics: both hold the id unless
+ -- prefer_name puts the name in service, and both are empty for a session
+ -- that never got as far as a route with a service
metrics.stream_connection_total = prometheus:counter("stream_connection_total",
"Total number of connections handled per stream route in APISIX",
- {"route"})
+ {"route", "service", "service_id"})
-- Keyed by listen_addr rather than by route: a session can end before any
-- stream route is matched, and the byte counters come from nginx, which
- -- only knows the listening address.
+ -- only knows the listening address. service and service_id split that
+ -- total further.
metrics.stream_active_connections = prometheus:gauge(
"stream_active_connections",
- "Number of stream sessions currently being proxied per listening address",
- {"listen_addr"})
+ "Number of stream sessions currently being proxied per listening address and service",
+ {"listen_addr", "service", "service_id"})
metrics.stream_status = prometheus:counter("stream_status",
"Stream sessions per termination status in APISIX",
- {"code", "listen_addr", "node"})
+ {"code", "listen_addr", "service", "service_id", "node"})
metrics.stream_bandwidth = prometheus:counter("stream_bandwidth",
"Total bandwidth in bytes proxied by the stream subsystem in APISIX",
- {"listen_addr", "type", "side"})
+ {"listen_addr", "service", "service_id", "type", "side"})
xrpc.init_metrics(prometheus)
end
@@ -317,6 +321,48 @@ local STREAM_PUBLISHED_PREFIX = "stream_bytes_published:"
local STREAM_PUBLISH_LOCK = "stream_bytes_publishing"
local STREAM_PUBLISH_LOCK_TTL = 10
+
+-- The same rule as the http metrics: the id, or the name with prefer_name.
+-- Resolved once per session: preread labels the zone with it and the log
+-- phase reuses it, so all four metrics name a session the same way even if
+-- its service is renamed while it is open.
+local function stream_service_labels(conf, ctx)
+ local labels = ctx.prometheus_stream_service
+ if labels then
+ return labels[1], labels[2]
+ end
+
+ local service, service_id = "", ""
+ local id = ctx.service_id
+ local name = ctx.service_name
+ if not id then
+ -- a route with an upstream_id keeps its service_id without the service
+ -- being merged in, and the http metrics still label it with it
+ id = ctx.matched_route.value.service_id
+ if id then
+ local fetched = conf.prefer_name == true and service_fetch(id)
+ name = fetched and fetched.value.name
+ end
+ end
+
+ if id then
+ service_id = tostring(id)
+ service = conf.prefer_name == true and name or service_id
+ end
+
+ ctx.prometheus_stream_service = {service, service_id}
+ return service, service_id
+end
+
+
+-- The zone is read where neither the session nor the plugin conf is at hand,
+-- so a session carries its label values into the zone itself; "" on a slot
+-- that was never labelled.
+local function stream_zone_labels(entry)
+ local labels = entry.labels
+ return labels[1] or "", labels[2] or ""
+end
+
local stream_metrics_lib
local stream_metrics_lib_checked = false
local stream_zone_unavailable = false
@@ -343,16 +389,19 @@ local function stream_metrics_zone()
end
-local function publish_stream_bytes(dict, listen_addr, direction, total)
+local function publish_stream_bytes(dict, listen_addr, service, service_id, direction,
+ total)
local field = direction[1]
if type(total) ~= "number" then
core.log.error("stream metrics zone reported no ", field, " for ",
- listen_addr)
+ listen_addr, " service ", service_id)
return
end
- local key = STREAM_PUBLISHED_PREFIX .. listen_addr .. ":" .. field
+ -- a service name can hold any character but this one
+ local key = STREAM_PUBLISHED_PREFIX .. listen_addr .. "\31" .. service .. "\31"
+ .. service_id .. "\31" .. field
local published = dict:get(key)
if not published then
@@ -384,7 +433,7 @@ local function publish_stream_bytes(dict, listen_addr, direction, total)
-- rebaselines rather than emitting a negative delta
if total > published then
metrics.stream_bandwidth:inc(total - published,
- gen_arr(listen_addr, direction[2], direction[3]))
+ gen_arr(listen_addr, service, service_id, direction[2], direction[3]))
end
end
@@ -430,14 +479,17 @@ local function collect_stream_zone_metrics()
for _, entry in ipairs(entries) do
local listen_addr = entry.listen_addr
+ local service, service_id = stream_zone_labels(entry)
-- the gauge carries no baseline and every reader writes the same
-- value, so it is published whether or not this one took the lock
- metrics.stream_active_connections:set(entry.active, gen_arr(listen_addr))
+ metrics.stream_active_connections:set(entry.active,
+ gen_arr(listen_addr, service, service_id))
if publishing then
for _, direction in ipairs(STREAM_BANDWIDTH_DIRECTIONS) do
- publish_stream_bytes(dict, listen_addr, direction, entry[direction[1]])
+ publish_stream_bytes(dict, listen_addr, service, service_id, direction,
+ entry[direction[1]])
end
end
end
@@ -944,7 +996,10 @@ function _M.stream_log(conf, ctx)
end
end
- metrics.stream_connection_total:inc(1, gen_arr(route_id))
+ -- empty when the session ended before a route with a service matched
+ local service, service_id = stream_service_labels(conf, ctx)
+
+ metrics.stream_connection_total:inc(1, gen_arr(route_id, service, service_id))
-- empty when the session ended before a node was picked
local node = ""
@@ -953,7 +1008,41 @@ function _M.stream_log(conf, ctx)
end
metrics.stream_status:inc(1, gen_arr(stream_status_code(ctx),
- stream_listen_addr(ctx), node))
+ stream_listen_addr(ctx), service, service_id, node))
+end
+
+
+-- logged once per worker, it would otherwise repeat on every session
+local stream_label_error_logged = false
+
+
+-- Labels the session in the metrics zone as it starts, so that its active
+-- count and every byte it moves from now on are accounted under its service
+-- while it is still open. A session that never gets here -- it ended before
+-- routing, its route has no service, or a plugin running before this one
+-- rejected it -- stays in the unlabelled total of its listen_addr.
+function _M.stream_preread(conf, ctx)
+ local service, service_id = stream_service_labels(conf, ctx)
+ if service_id == "" then
+ return
+ end
+
+ local lib = stream_metrics_zone()
+ if not lib then
+ return
+ end
+
+ -- "not accounted" is a listening address the zone does not count, such as
+ -- a unix socket. Anything else -- a full zone, or labels too long for it --
+ -- leaves the session in the unlabelled total, while apisix_stream_status
+ -- still has its service.
+ local ok, err = lib.set_labels({service, service_id})
+ if not ok and err ~= "not accounted" and not stream_label_error_logged then
+ stream_label_error_logged = true
+ core.log.warn("failed to label stream sessions, first seen on service ",
+ service_id, ", they are only counted in the listen_addr ",
+ "total: ", err)
+ end
end
diff --git a/apisix/stream/plugins/prometheus.lua b/apisix/stream/plugins/prometheus.lua
index 6b30ca252..c7fa9fb06 100644
--- a/apisix/stream/plugins/prometheus.lua
+++ b/apisix/stream/plugins/prometheus.lua
@@ -34,6 +34,7 @@ local _M = {
version = 0.1,
priority = 500,
name = plugin_name,
+ preread = exporter.stream_preread,
log = exporter.stream_log,
destroy = exporter.destroy,
init = exporter.stream_init,
diff --git a/ci/linux-install-openresty.sh b/ci/linux-install-openresty.sh
index c02fd180d..51bdf0fae 100755
--- a/ci/linux-install-openresty.sh
+++ b/ci/linux-install-openresty.sh
@@ -61,7 +61,7 @@ else
sudo apt-get -y update --fix-missing
sudo apt-get install -y build-essential gcc g++ cpanminus libxml2-dev libxslt-dev
- if [ "$APISIX_RUNTIME" != "1.3.18" ]; then
+ if [ "$APISIX_RUNTIME" != "1.3.19" ]; then
echo "Please update the apisix-runtime-debug checksum for APISIX_RUNTIME=$APISIX_RUNTIME" >&2
exit 1
fi
@@ -69,11 +69,11 @@ else
case "$ARCH" in
x86_64|amd64)
DEB_ARCH="amd64"
- EXPECTED_SHA256="0d7cbe27cd0303c6f6b3cad27338a697cf2e2e809ceeac9a57b2d1fb78a3a20e"
+ EXPECTED_SHA256="0f350a142c915a70d4501d68259a1506ffdd8efd6a1464802c8935b1f36fdf3b"
;;
arm64|aarch64)
DEB_ARCH="arm64"
- EXPECTED_SHA256="a7cf5040837e4d34f456e97ff9b858ce64bf95e3b2f647f5d3021de5a9770741"
+ EXPECTED_SHA256="67fc887818f2899874e26fc539b9f49c78c7f8e73aed495683df9b694250db54"
;;
*)
echo "Unsupported architecture: $ARCH" >&2
diff --git a/docs/en/latest/plugins/prometheus.md b/docs/en/latest/plugins/prometheus.md
index 95d89f02f..2b183020e 100644
--- a/docs/en/latest/plugins/prometheus.md
+++ b/docs/en/latest/plugins/prometheus.md
@@ -202,6 +202,20 @@ The following labels are used to differentiate `apisix_http_status` metrics.
| llm_model | Effective target model for the AI request. Uses the model configured on the AI instance when present; otherwise uses the model requested by the client. Empty for traditional HTTP traffic. |
| response_source | Response origin: `apisix` for responses generated by APISIX, `nginx` for NGINX proxy errors, or `upstream` for responses received from the Upstream. |
+### Service labels on the Stream metrics
+
+All four Stream metrics carry `service` and `service_id`, following the same rule as the HTTP metrics: `service_id` is the ID of the Service the session's Stream Route belongs to, and `service` is that ID too, or the Service's name when `prefer_name` is `true` on the Route's `prometheus` Plugin. Both are empty for a session that never reached a Stream Route with a Service, for example one that failed its TLS handshake or matched no Route. Such sessions are only counted in the per `listen [...]
+
+`apisix_stream_active_connections` and `apisix_stream_bandwidth` are split per Service while sessions are still open: the Stream `prometheus` Plugin labels each session with its Service when it starts, and from then on its active count and the bytes it moves are accounted under that Service.
+
+### Labels for `apisix_stream_connection_total`
+
+| Name | Description |
+| ----------- | --------------------------------------------------------------------------------------- |
+| route | ID of the matched Stream Route, or its name when `prefer_name` is `true`. |
+| service | ID of the Service of the matched Stream Route when `prefer_name` is `false` (default), and name of the Service when `prefer_name` is `true`. Empty when the session did not reach a Stream Route that belongs to a Service. |
+| service_id | ID of the Service of the matched Stream Route. Empty when the session did not reach a Stream Route that belongs to a Service. |
+
### Labels for `apisix_stream_active_connections`
The gauge is incremented when a session is accepted and decremented when it
@@ -210,6 +224,8 @@ ends, so it reflects live concurrency without waiting for sessions to finish.
| Name | Description |
| ----------- | --------------------------------------------------------------------------------------- |
| listen_addr | Listening address the client connected to, for example `0.0.0.0:9100`. |
+| service | ID of the Service of the matched Stream Route when `prefer_name` is `false` (default), and name of the Service when `prefer_name` is `true`. Empty when the session did not reach a Stream Route that belongs to a Service. |
+| service_id | ID of the Service of the matched Stream Route. Empty when the session did not reach a Stream Route that belongs to a Service. |
### Labels for `apisix_stream_status`
@@ -225,6 +241,8 @@ clean close. No synthetic code is introduced.
| ----------- | --------------------------------------------------------------------------------------- |
| code | How the session ended: `200` for a normal close, worker shutdown, or a missing or unrecognized termination reason; `400` for a client-side problem such as a reset or invalid preread data; `403` when rejected by an access rule; `500` for an internal error; `502` for an upstream or transport problem such as a connect failure, reset, or idle timeout; `503` when rejected by a connection limit. |
| listen_addr | Listening address the client connected to, for example `0.0.0.0:9100`. |
+| service | ID of the Service of the matched Stream Route when `prefer_name` is `false` (default), and name of the Service when `prefer_name` is `true`. Empty when the session did not reach a Stream Route that belongs to a Service. |
+| service_id | ID of the Service of the matched Stream Route. Empty when the session did not reach a Stream Route that belongs to a Service. |
| node | Address of the upstream node used, empty when no node was selected. |
For UDP only a subset of the codes occurs, since UDP has no close, FIN or
@@ -239,6 +257,8 @@ counted; the HTTP subsystem cannot contribute to it.
| Name | Description |
| ----------- | --------------------------------------------------------------------------------------- |
| listen_addr | Listening address the client connected to, for example `0.0.0.0:9100`. |
+| service | ID of the Service of the matched Stream Route when `prefer_name` is `false` (default), and name of the Service when `prefer_name` is `true`. Empty when the session did not reach a Stream Route that belongs to a Service. |
+| service_id | ID of the Service of the matched Stream Route. Empty when the session did not reach a Stream Route that belongs to a Service. |
| side | Which connection the bytes crossed: `downstream` between APISIX and the client, `upstream` between APISIX and the upstream. |
| type | Direction relative to APISIX, matching `apisix_bandwidth`: `ingress` for bytes APISIX received, `egress` for bytes APISIX sent. |
@@ -763,28 +783,31 @@ You should see an output similar to the following:
```text
# HELP apisix_stream_connection_total Total number of connections handled per Stream Route in APISIX
# TYPE apisix_stream_connection_total counter
-apisix_stream_connection_total{route="prometheus-route"} 1
-# HELP apisix_stream_active_connections Number of stream sessions currently being proxied per listening address
+apisix_stream_connection_total{route="prometheus-route",service="",service_id=""} 1
+# HELP apisix_stream_active_connections Number of stream sessions currently being proxied per listening address and service
# TYPE apisix_stream_active_connections gauge
-apisix_stream_active_connections{listen_addr="0.0.0.0:9100"} 0
+apisix_stream_active_connections{listen_addr="0.0.0.0:9100",service="",service_id=""} 0
# HELP apisix_stream_status Stream sessions per termination status in APISIX
# TYPE apisix_stream_status counter
-apisix_stream_status{code="200",listen_addr="0.0.0.0:9100",node="54.237.103.220:80"} 1
+apisix_stream_status{code="200",listen_addr="0.0.0.0:9100",service="",service_id="",node="54.237.103.220:80"} 1
# HELP apisix_stream_bandwidth Total bandwidth in bytes proxied by the stream subsystem in APISIX
# TYPE apisix_stream_bandwidth counter
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="ingress",side="downstream"} 78
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="egress",side="downstream"} 219
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="egress",side="upstream"} 78
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="ingress",side="upstream"} 219
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="ingress",side="downstream"} 78
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="egress",side="downstream"} 219
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="egress",side="upstream"} 78
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="ingress",side="upstream"} 219
```
-The exact Upstream address and byte counts depend on the request. The active-connections gauge is `0` above because the request completed before the scrape; scrape while a connection remains open to observe a positive value.
+The Stream Route in this example does not belong to a Service, so `service` and `service_id` are empty. The exact Upstream address and byte counts depend on the request. The active-connections gauge is `0` above because the request completed before the scrape; scrape while a connection remains open to observe a positive value.
:::note
`apisix_stream_active_connections` and `apisix_stream_bandwidth` are backed by
an NGINX shared memory zone, sized by `nginx_config.stream.metrics_zone_size`
(default `1m`). They require APISIX-Runtime; on a runtime without it the two
-metrics are simply not published.
+metrics are simply not published. The zone holds one slot per listening address
+plus one per Service seen on it, about 760 slots for `1m`; at most three
+quarters of them go to Services. When they run out, sessions of a new Service
+are only counted in their `listen_addr` total and a warning is logged.
:::
diff --git a/docs/zh/latest/plugins/prometheus.md b/docs/zh/latest/plugins/prometheus.md
index 86081f9ff..ce772bfa7 100644
--- a/docs/zh/latest/plugins/prometheus.md
+++ b/docs/zh/latest/plugins/prometheus.md
@@ -202,6 +202,20 @@ Prometheus 中有不同类型的指标。要了解它们之间的区别,请参
| llm_model | AI 请求实际使用的目标模型。优先使用 AI 实例中配置的模型,否则使用客户端请求的模型;传统 HTTP 流量中为空字符串。 |
| response_source | 响应来源:`apisix` 表示由 APISIX 生成,`nginx` 表示 NGINX 代理错误,`upstream` 表示来自上游服务的响应。 |
+### Stream 指标的 Service 标签
+
+四个 Stream 指标都带有 `service` 和 `service_id` 标签,取值规则与 HTTP 指标相同:`service_id` 为会话所匹配 Stream Route 所属 Service 的 ID;`service` 默认也是该 ID,当 Route 上 `prometheus` 插件的 `prefer_name` 为 `true` 时为 Service 的名称。会话未到达属于某个 Service 的 Stream Route 时(例如 TLS 握手失败或未匹配到任何 Route),两个标签均为空,这类会话只计入对应 `listen_addr` 的总量,因此按 `service` 求和的结果与此前的总量一致。
+
+`apisix_stream_active_connections` 和 `apisix_stream_bandwidth` 在会话进行中即按 Service 拆分:Stream `prometheus` 插件在会话开始时为其标记所属 Service,此后该会话的活跃计数和传输的字节都计入该 Service。
+
+### `apisix_stream_connection_total` 的标签
+
+| 名称 | 描述 |
+| --- | --- |
+| route | 匹配的 Stream Route 的 ID;`prefer_name` 为 `true` 时为其名称。 |
+| service | `prefer_name` 为 `false`(默认)时为匹配的 Stream Route 所属 Service 的 ID,为 `true` 时为该 Service 的名称。会话未到达属于某个 Service 的 Stream Route 时为空。 |
+| service_id | 匹配的 Stream Route 所属 Service 的 ID。会话未到达属于某个 Service 的 Stream Route 时为空。 |
+
### `apisix_stream_active_connections` 的标签
接受会话时,该 gauge 会递增;会话结束时递减,因此无需等到会话结束即可反映实时并发量。
@@ -209,6 +223,8 @@ Prometheus 中有不同类型的指标。要了解它们之间的区别,请参
| 名称 | 描述 |
| --- | --- |
| listen_addr | 客户端连接的监听地址,例如 `0.0.0.0:9100`。 |
+| service | `prefer_name` 为 `false`(默认)时为匹配的 Stream Route 所属 Service 的 ID,为 `true` 时为该 Service 的名称。会话未到达属于某个 Service 的 Stream Route 时为空。 |
+| service_id | 匹配的 Stream Route 所属 Service 的 ID。会话未到达属于某个 Service 的 Stream Route 时为空。 |
### `apisix_stream_status` 的标签
@@ -218,6 +234,8 @@ Prometheus 中有不同类型的指标。要了解它们之间的区别,请参
| --- | --- |
| code | 会话结束方式:`200` 表示正常关闭、worker 关闭,或终止原因缺失或无法识别;`400` 表示客户端重置或预读数据无效等客户端问题;`403` 表示被访问规则拒绝;`500` 表示内部错误;`502` 表示连接失败、重置或空闲超时等上游或传输问题;`503` 表示被连接数限制拒绝。 |
| listen_addr | 客户端连接的监听地址,例如 `0.0.0.0:9100`。 |
+| service | `prefer_name` 为 `false`(默认)时为匹配的 Stream Route 所属 Service 的 ID,为 `true` 时为该 Service 的名称。会话未到达属于某个 Service 的 Stream Route 时为空。 |
+| service_id | 匹配的 Stream Route 所属 Service 的 ID。会话未到达属于某个 Service 的 Stream Route 时为空。 |
| node | 使用的上游节点地址;未选择节点时为空。 |
UDP 没有关闭、FIN 或重置信号,因此只会出现其中一部分状态代码。
@@ -229,6 +247,8 @@ UDP 没有关闭、FIN 或重置信号,因此只会出现其中一部分状态
| 名称 | 描述 |
| --- | --- |
| listen_addr | 客户端连接的监听地址,例如 `0.0.0.0:9100`。 |
+| service | `prefer_name` 为 `false`(默认)时为匹配的 Stream Route 所属 Service 的 ID,为 `true` 时为该 Service 的名称。会话未到达属于某个 Service 的 Stream Route 时为空。 |
+| service_id | 匹配的 Stream Route 所属 Service 的 ID。会话未到达属于某个 Service 的 Stream Route 时为空。 |
| side | 字节经过的连接侧:`downstream` 表示 APISIX 与客户端之间,`upstream` 表示 APISIX 与上游之间。 |
| type | 相对 APISIX 的方向,与 `apisix_bandwidth` 一致:`ingress` 表示 APISIX 接收的字节,`egress` 表示 APISIX 发送的字节。 |
@@ -750,25 +770,25 @@ curl "http://127.0.0.1:9091/apisix/prometheus/metrics"
```text
# HELP apisix_stream_connection_total APISIX 中每个 Stream Route 处理的总连接数
# TYPE apisix_stream_connection_total counter
-apisix_stream_connection_total{route="prometheus-route"} 1
-# HELP apisix_stream_active_connections Number of stream sessions currently being proxied per listening address
+apisix_stream_connection_total{route="prometheus-route",service="",service_id=""} 1
+# HELP apisix_stream_active_connections Number of stream sessions currently being proxied per listening address and service
# TYPE apisix_stream_active_connections gauge
-apisix_stream_active_connections{listen_addr="0.0.0.0:9100"} 0
+apisix_stream_active_connections{listen_addr="0.0.0.0:9100",service="",service_id=""} 0
# HELP apisix_stream_status Stream sessions per termination status in APISIX
# TYPE apisix_stream_status counter
-apisix_stream_status{code="200",listen_addr="0.0.0.0:9100",node="54.237.103.220:80"} 1
+apisix_stream_status{code="200",listen_addr="0.0.0.0:9100",service="",service_id="",node="54.237.103.220:80"} 1
# HELP apisix_stream_bandwidth Total bandwidth in bytes proxied by the stream subsystem in APISIX
# TYPE apisix_stream_bandwidth counter
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="ingress",side="downstream"} 78
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="egress",side="downstream"} 219
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="egress",side="upstream"} 78
-apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",type="ingress",side="upstream"} 219
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="ingress",side="downstream"} 78
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="egress",side="downstream"} 219
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="egress",side="upstream"} 78
+apisix_stream_bandwidth{listen_addr="0.0.0.0:9100",service="",service_id="",type="ingress",side="upstream"} 219
```
-实际的上游地址和字节数取决于请求。上例中的活跃连接 gauge 为 `0`,因为抓取指标时请求已完成;如需观察正值,请在连接保持打开时抓取指标。
+本例中的 Stream Route 不属于任何 Service,因此 `service` 和 `service_id` 为空。实际的上游地址和字节数取决于请求。上例中的活跃连接 gauge 为 `0`,因为抓取指标时请求已完成;如需观察正值,请在连接保持打开时抓取指标。
:::note
-`apisix_stream_active_connections` 和 `apisix_stream_bandwidth` 使用由 `nginx_config.stream.metrics_zone_size` 配置的 NGINX 共享内存区,默认大小为 `1m`。这两个指标依赖 APISIX-Runtime;如果运行时不提供对应模块,则不会发布这两个指标。
+`apisix_stream_active_connections` 和 `apisix_stream_bandwidth` 使用由 `nginx_config.stream.metrics_zone_size` 配置的 NGINX 共享内存区,默认大小为 `1m`。这两个指标依赖 APISIX-Runtime;如果运行时不提供对应模块,则不会发布这两个指标。该共享内存区为每个监听地址及其上出现的每个 Service 各分配一个 slot,`1m` 约可容纳 760 个 slot,其中最多四分之三分配给 Service。slot 用尽后,新 Service 的会话只计入其 `listen_addr` 的总量,并记录一条警告日志。
:::
diff --git a/t/cli/test_prometheus_stream.sh b/t/cli/test_prometheus_stream.sh
index 72fb96759..fcbfb5a2e 100755
--- a/t/cli/test_prometheus_stream.sh
+++ b/t/cli/test_prometheus_stream.sh
@@ -64,7 +64,7 @@ deadline=$(( $(date +%s) + 20 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
curl -s --connect-timeout 1 --max-time 2 http://127.0.0.1:9100 >/dev/null 2>&1 || true
if curl -s --connect-timeout 1 --max-time 2 http://127.0.0.1:9091/apisix/prometheus/metrics \
- | grep -qE 'apisix_stream_connection_total\{route="1"\} [1-9][0-9]*'; then
+ | grep -qE 'apisix_stream_connection_total\{route="1",service="",service_id=""\} [1-9][0-9]*'; then
ok=1
break
fi
@@ -106,7 +106,7 @@ out=""
while [ "$(date +%s)" -lt "$deadline" ]; do
curl -s --connect-timeout 1 --max-time 2 http://127.0.0.1:9100 >/dev/null 2>&1 || true
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_stream_connection_total\{route="1"\} [1-9][0-9]*'; then
+ if echo "$out" | grep -qE 'apisix_stream_connection_total\{route="1",service="",service_id=""\} [1-9][0-9]*'; then
ok=1
break
fi
diff --git a/t/stream-plugin/prometheus-metrics-service.t b/t/stream-plugin/prometheus-metrics-service.t
new file mode 100644
index 000000000..5de47102a
--- /dev/null
+++ b/t/stream-plugin/prometheus-metrics-service.t
@@ -0,0 +1,580 @@
+#
+# 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.
+#
+BEGIN {
+ if ($ENV{TEST_NGINX_CHECK_LEAK}) {
+ $SkipReason = "unavailable for the hup tests";
+
+ } else {
+ $ENV{TEST_NGINX_USE_HUP} = 1;
+ undef $ENV{TEST_NGINX_USE_STAP};
+ }
+}
+
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ # the endpoint serves a cache the privileged agent refills on this
+ # interval, see t/stream-plugin/prometheus-metrics.t
+ my $extra_yaml_config = <<_EOC_;
+stream_plugins:
+ - prometheus
+plugin_attr:
+ prometheus:
+ refresh_interval: 0.5
+_EOC_
+
+ $block->set_value("extra_yaml_config", $extra_yaml_config);
+
+ if (!defined $block->request) {
+ $block->set_value("request", "GET /t");
+ }
+
+ # see t/stream-plugin/prometheus-metrics.t: a scrape without the stream
+ # block has no zone to read, and the reload into it drops the zone
+ if ($block->request =~ m{/apisix/prometheus/metrics}) {
+ $block->set_value("stream_enable", 1);
+ }
+
+ # an upstream that answers and then holds the session open, so that a
+ # scrape can observe it while it is live. Only for the probes: a stream
+ # block makes Test::Nginx install its own `location = /t`.
+ if ($block->request =~ m{/probe}) {
+ my $extra_stream_config = <<_EOC_;
+server {
+ listen 1993;
+ content_by_lua_block {
+ local sock = ngx.req.socket()
+ sock:receive("1")
+ ngx.say("hello world")
+ ngx.flush(true)
+ ngx.sleep(10)
+ }
+}
+_EOC_
+
+ $block->set_value("extra_stream_config", $extra_stream_config);
+ }
+
+ # The scrapes go over a raw socket: capturing into an APISIX route leaves
+ # the upstream connect without a usable api_ctx.
+ my $extra_init_by_lua = <<_EOC_;
+ function _G.scrape()
+ -- let the privileged agent refill the cache with what happened so far
+ ngx.sleep(1.2)
+
+ local sock = ngx.socket.tcp()
+ local ok, err = sock:connect("127.0.0.1", 1984)
+ if not ok then
+ return nil, "scrape connect: " .. err
+ end
+
+ ok, err = sock:send("GET /apisix/prometheus/metrics HTTP/1.0\\r\\n"
+ .. "Host: 127.0.0.1\\r\\n\\r\\n")
+ if not ok then
+ return nil, "scrape send: " .. err
+ end
+
+ local body, rerr, partial = sock:receive("*a")
+ sock:close()
+ body = body or partial
+ if not body then
+ return nil, "scrape: " .. rerr
+ end
+
+ return body
+ end
+
+ -- Opens a session on 1985 and scrapes while it is live. A scrape runs
+ -- first so that the bandwidth baselines are taken before the session
+ -- exists: the first read of the zone only baselines what it finds.
+ function _G.scrape_live_session()
+ local body, err = scrape()
+ if not body then
+ return nil, err
+ end
+
+ local sock = ngx.socket.tcp()
+ local ok
+ ok, err = sock:connect("127.0.0.1", 1985)
+ if not ok then
+ return nil, "connect: " .. err
+ end
+
+ local bytes
+ bytes, err = sock:send("hello")
+ if not bytes then
+ return nil, "send: " .. err
+ end
+
+ local line
+ line, err = sock:receive("*l")
+ if not line then
+ return nil, "receive: " .. err
+ end
+
+ body, err = scrape()
+ sock:close()
+ return body, err
+ end
+
+ -- the value of the series whose labels are exactly `labels`
+ function _G.series_value(body, name, labels)
+ local prefix = name .. "{" .. labels .. "} "
+ for line in body:gmatch("[^\\n]+") do
+ if line:sub(1, #prefix) == prefix then
+ return line:sub(#prefix + 1)
+ end
+ end
+ return "no-series"
+ end
+
+ function _G.svc_a_ingress(body)
+ return series_value(body, "apisix_stream_bandwidth",
+ 'listen_addr="0.0.0.0:1985",service="svc-a",service_id="svc-a",'
+ .. 'type="ingress",side="downstream"')
+ end
+
+ function _G.active_on(body, service, service_id)
+ return series_value(body, "apisix_stream_active_connections",
+ 'listen_addr="0.0.0.0:1985",service="' .. service
+ .. '",service_id="' .. service_id .. '"')
+ end
+_EOC_
+
+ $block->set_value("extra_init_by_lua", $extra_init_by_lua);
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: pre-create the metrics endpoint, a service and a stream route under it
+--- config
+ location /t {
+ content_by_lua_block {
+ local data = {
+ {
+ url = "/apisix/admin/routes/metrics",
+ data = [[{
+ "plugins": {
+ "public-api": {}
+ },
+ "uri": "/apisix/prometheus/metrics"
+ }]]
+ },
+ {
+ url = "/apisix/admin/services/svc-a",
+ data = [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1993,
+ "weight": 1
+ }]
+ }
+ }]]
+ },
+ {
+ url = "/apisix/admin/stream_routes/1",
+ data = [[{
+ "plugins": {
+ "prometheus": {}
+ },
+ "service_id": "svc-a"
+ }]]
+ }
+ }
+
+ local t = require("lib.test_admin").test
+
+ for _, data in ipairs(data) do
+ local code, body = t(data.url, ngx.HTTP_PUT, data.data)
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+ end
+ }
+ }
+--- response_body
+
+
+
+=== TEST 2: a live session is split out under its service_id
+The session is labelled in preread, so the service series carries it and the
+unlabelled series of the same listen_addr does not. Like any zone slot, the
+service's slot only takes a baseline the first time it is read, so its bytes
+are counted from its second session on.
+--- config
+ location /probe {
+ content_by_lua_block {
+ -- the route from TEST 1 has to reach the stream workers first
+ ngx.sleep(1.5)
+
+ local body, err = scrape_live_session()
+ if not body then
+ ngx.say(err)
+ return
+ end
+ ngx.say("first session bandwidth=", svc_a_ingress(body))
+
+ body, err = scrape_live_session()
+ if not body then
+ ngx.say(err)
+ return
+ end
+
+ ngx.say("service live=", active_on(body, "svc-a", "svc-a"))
+ ngx.say("unlabelled live=", active_on(body, "", ""))
+
+ local bw = tonumber(svc_a_ingress(body))
+ ngx.say("service bandwidth=", bw and tonumber(bw) > 0 and "counted"
+ or bw or "no-series")
+
+ ngx.sleep(1.5)
+ }
+ }
+--- request
+GET /probe
+--- stream_enable
+--- timeout: 20
+--- response_body
+first session bandwidth=no-series
+service live=1
+unlabelled live=0
+service bandwidth=counted
+
+
+
+=== TEST 3: once the session is gone its service gauge is back to zero
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_active_connections\{listen_addr="0\.0\.0\.0:1985",service="svc-a",service_id="svc-a"\} 0$/m
+--- no_error_log
+[error]
+
+
+
+=== TEST 4: the service carries the gateway to upstream bytes
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",service="svc-a",service_id="svc-a",type="egress",side="upstream"\} [1-9]\d*/
+--- no_error_log
+[error]
+
+
+
+=== TEST 5: the service carries the upstream to gateway bytes
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",service="svc-a",service_id="svc-a",type="ingress",side="upstream"\} [1-9]\d*/
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: a route without a service keeps its sessions in the unlabelled total
+--- config
+ location /probe {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code = t("/apisix/admin/stream_routes/1", ngx.HTTP_PUT, [[{
+ "plugins": {
+ "prometheus": {}
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1993,
+ "weight": 1
+ }]
+ }
+ }]])
+ if code > 300 then
+ ngx.say("route: ", code)
+ return
+ end
+
+ ngx.sleep(1.5)
+
+ local body, err = scrape_live_session()
+ if not body then
+ ngx.say(err)
+ return
+ end
+
+ ngx.say("service live=", active_on(body, "svc-a", "svc-a"))
+ ngx.say("unlabelled live=", active_on(body, "", ""))
+
+ ngx.sleep(1.5)
+ }
+ }
+--- request
+GET /probe
+--- stream_enable
+--- timeout: 20
+--- response_body
+service live=0
+unlabelled live=1
+
+
+
+=== TEST 7: with prefer_name the service label carries the name
+The session is labelled when it starts, so the live session is already
+reported under the name. The name is its own zone slot: the gauge left under
+the id label by the earlier cases stays, at 0.
+--- config
+ location /probe {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code = t("/apisix/admin/services/svc-a", ngx.HTTP_PUT, [[{
+ "name": "Order TCP",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1993,
+ "weight": 1
+ }]
+ }
+ }]])
+ if code > 300 then
+ ngx.say("service: ", code)
+ return
+ end
+
+ code = t("/apisix/admin/stream_routes/1", ngx.HTTP_PUT, [[{
+ "plugins": {
+ "prometheus": {
+ "prefer_name": true
+ }
+ },
+ "service_id": "svc-a"
+ }]])
+ if code > 300 then
+ ngx.say("route: ", code)
+ return
+ end
+
+ ngx.sleep(1.5)
+
+ -- the first session under the name only baselines its new slot
+ local body, err = scrape_live_session()
+ if not body then
+ ngx.say(err)
+ return
+ end
+
+ body, err = scrape_live_session()
+ if not body then
+ ngx.say(err)
+ return
+ end
+
+ ngx.say("name live=", active_on(body, "Order TCP", "svc-a"))
+ ngx.say("id label live=", active_on(body, "svc-a", "svc-a"))
+
+ local bw = tonumber(series_value(body, "apisix_stream_bandwidth",
+ 'listen_addr="0.0.0.0:1985",service="Order TCP",service_id="svc-a",'
+ .. 'type="ingress",side="downstream"'))
+ ngx.say("name bandwidth=", bw and tonumber(bw) > 0 and "counted"
+ or bw or "no-series")
+
+ ngx.sleep(1.5)
+ }
+ }
+--- request
+GET /probe
+--- stream_enable
+--- timeout: 20
+--- response_body
+name live=1
+id label live=0
+name bandwidth=counted
+
+
+
+=== TEST 8: the termination status follows the same rule
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_status\{code="200",listen_addr="0\.0\.0\.0:1985",service="Order TCP",service_id="svc-a",node="127\.0\.0\.1:1993"\} 2$/m
+--- no_error_log
+[error]
+
+
+
+=== TEST 9: a slot that lost its baseline rebaselines instead of replaying
+A baseline can go missing while the rest of the dict stays, when it is
+evicted or could not be written. Counting that slot from zero would land its
+whole lifetime total in one interval; it has to take a new baseline instead,
+and later traffic is counted as usual.
+--- config
+ location /probe {
+ content_by_lua_block {
+ -- back to the id as the service label
+ local t = require("lib.test_admin").test
+ local code = t("/apisix/admin/services/svc-a", ngx.HTTP_PUT, [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1993,
+ "weight": 1
+ }]
+ }
+ }]])
+ if code > 300 then
+ ngx.say("service: ", code)
+ return
+ end
+
+ -- a series with a baseline behind it
+ local body, err = scrape_live_session()
+ if not body then
+ ngx.say(err)
+ return
+ end
+ body, err = scrape()
+ if not body then
+ ngx.say(err)
+ return
+ end
+ local before = tonumber(svc_a_ingress(body))
+
+ ngx.shared["prometheus-metrics"]:delete(
+ "stream_bytes_published:0.0.0.0:1985\31svc-a\31svc-a\31downstream_ingress")
+
+ body, err = scrape()
+ if not body then
+ ngx.say(err)
+ return
+ end
+ ngx.say("after the lost baseline: ", tonumber(svc_a_ingress(body)) == before)
+
+ body, err = scrape_live_session()
+ if not body then
+ ngx.say(err)
+ return
+ end
+ ngx.say("after one more session: +", tonumber(svc_a_ingress(body)) - before)
+
+ ngx.sleep(1.5)
+ }
+ }
+--- request
+GET /probe
+--- stream_enable
+--- timeout: 20
+--- response_body
+after the lost baseline: true
+after one more session: +5
+
+
+
+=== TEST 10: a session keeps the service label it started with
+A route with an upstream_id and a service_id resolves its service name by
+itself. The name picked when the session starts labels it in the zone and is
+reused when it ends, so a rename while it is open does not split the session
+between two names across the metrics.
+--- config
+ location /probe {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local function put(uri, body)
+ local code, res = t(uri, ngx.HTTP_PUT, body)
+ if code > 300 then
+ ngx.say(uri, ": ", res)
+ end
+ return code <= 300
+ end
+
+ if not put("/apisix/admin/services/svc-b", [[{
+ "name": "Billing",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{"host": "127.0.0.1", "port": 1993, "weight": 1}]
+ }
+ }]]) or not put("/apisix/admin/upstreams/up-1", [[{
+ "type": "roundrobin",
+ "nodes": [{"host": "127.0.0.1", "port": 1993, "weight": 1}]
+ }]]) or not put("/apisix/admin/stream_routes/1", [[{
+ "plugins": {
+ "prometheus": {
+ "prefer_name": true
+ }
+ },
+ "upstream_id": "up-1",
+ "service_id": "svc-b"
+ }]]) then
+ return
+ end
+
+ ngx.sleep(1.5)
+
+ local sock = ngx.socket.tcp()
+ assert(sock:connect("127.0.0.1", 1985))
+ assert(sock:send("hello"))
+ assert(sock:receive("*l"))
+
+ -- renamed while the session is open
+ if not put("/apisix/admin/services/svc-b", [[{
+ "name": "Billing v2",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{"host": "127.0.0.1", "port": 1993, "weight": 1}]
+ }
+ }]]) then
+ return
+ end
+ ngx.sleep(1.5)
+
+ assert(sock:close())
+
+ local body, err = scrape()
+ if not body then
+ ngx.say(err)
+ return
+ end
+
+ local status = 'code="200",listen_addr="0.0.0.0:1985",service="%s",'
+ .. 'service_id="svc-b",node="127.0.0.1:1993"'
+ ngx.say("status under the first name: ",
+ series_value(body, "apisix_stream_status", status:format("Billing")))
+ ngx.say("status under the new name: ",
+ series_value(body, "apisix_stream_status", status:format("Billing v2")))
+ }
+ }
+--- request
+GET /probe
+--- stream_enable
+--- timeout: 20
+--- response_body
+status under the first name: 1
+status under the new name: no-series
diff --git a/t/stream-plugin/prometheus-metrics.t b/t/stream-plugin/prometheus-metrics.t
index f7ecb83e4..f398cf80e 100644
--- a/t/stream-plugin/prometheus-metrics.t
+++ b/t/stream-plugin/prometheus-metrics.t
@@ -139,7 +139,7 @@ hello world
--- request
GET /apisix/prometheus/metrics
--- response_body eval
-qr/apisix_stream_status\{code="200",listen_addr="[^"]+",node="127.0.0.1:1995"\} 1$/m
+qr/apisix_stream_status\{code="200",listen_addr="[^"]+",service="",service_id="",node="127.0.0.1:1995"\} 1$/m
@@ -269,7 +269,7 @@ own rather than in one order-dependent pattern.
--- request
GET /apisix/prometheus/metrics
--- response_body_like eval
-qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",type="ingress",side="downstream"\} [1-9]\d*/
+qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",service="",service_id="",type="ingress",side="downstream"\} [1-9]\d*/
--- no_error_log
[error]
@@ -281,7 +281,7 @@ own rather than in one order-dependent pattern.
--- request
GET /apisix/prometheus/metrics
--- response_body_like eval
-qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",type="egress",side="downstream"\} [1-9]\d*/
+qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",service="",service_id="",type="egress",side="downstream"\} [1-9]\d*/
--- no_error_log
[error]
@@ -293,7 +293,7 @@ own rather than in one order-dependent pattern.
--- request
GET /apisix/prometheus/metrics
--- response_body_like eval
-qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",type="egress",side="upstream"\} [1-9]\d*/
+qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",service="",service_id="",type="egress",side="upstream"\} [1-9]\d*/
--- no_error_log
[error]
@@ -305,7 +305,7 @@ own rather than in one order-dependent pattern.
--- request
GET /apisix/prometheus/metrics
--- response_body_like eval
-qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",type="ingress",side="upstream"\} [1-9]\d*/
+qr/apisix_stream_bandwidth\{listen_addr="0\.0\.0\.0:1985",service="",service_id="",type="ingress",side="upstream"\} [1-9]\d*/
--- no_error_log
[error]
@@ -317,7 +317,7 @@ and outlived another tick, so the published value has to be 0 by now.
--- request
GET /apisix/prometheus/metrics
--- response_body_like eval
-qr/apisix_stream_active_connections\{listen_addr="0\.0\.0\.0:1985"\} 0$/m
+qr/apisix_stream_active_connections\{listen_addr="0\.0\.0\.0:1985",service="",service_id=""\} 0$/m
--- no_error_log
[error]
@@ -363,7 +363,7 @@ connect() failed
--- request
GET /apisix/prometheus/metrics
--- response_body eval
-qr/apisix_stream_status\{code="502",listen_addr="[^"]+",node="127.0.0.1:1979"\} 1$/m
+qr/apisix_stream_status\{code="502",listen_addr="[^"]+",service="",service_id="",node="127.0.0.1:1979"\} 1$/m
@@ -431,6 +431,202 @@ pins that for the same case -- so an idle timeout has to reach the metric as
--- request
GET /apisix/prometheus/metrics
--- response_body_like eval
-qr/apisix_stream_status\{code="502",listen_addr="0\.0\.0\.0:1985",node="127\.0\.0\.1:1993"\}/
+qr/apisix_stream_status\{code="502",listen_addr="0\.0\.0\.0:1985",service="",service_id="",node="127\.0\.0\.1:1993"\}/
--- no_error_log
[error]
+
+
+
+=== TEST 16: move the route under a service
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t("/apisix/admin/services/svc-a", ngx.HTTP_PUT, [[{
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1995,
+ "weight": 1
+ }]
+ }
+ }]])
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+
+ code, body = t("/apisix/admin/stream_routes/1", ngx.HTTP_PUT, [[{
+ "plugins": {
+ "prometheus": {}
+ },
+ "service_id": "svc-a"
+ }]])
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+ }
+ }
+--- response_body
+
+
+
+=== TEST 17: proxy a session through the service
+--- stream_request
+hello
+--- stream_response
+hello world
+
+
+
+=== TEST 18: the termination status carries the service
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_status\{code="200",listen_addr="0\.0\.0\.0:1985",service="svc-a",service_id="svc-a",node="127\.0\.0\.1:1995"\} 1$/m
+
+
+
+=== TEST 19: and so does the connection count
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_connection_total\{route="1",service="svc-a",service_id="svc-a"\} 1$/m
+
+
+
+=== TEST 20: name the service and ask for names on the route
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t("/apisix/admin/services/svc-a", ngx.HTTP_PUT, [[{
+ "name": "Order TCP",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1995,
+ "weight": 1
+ }]
+ }
+ }]])
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+
+ code, body = t("/apisix/admin/stream_routes/1", ngx.HTTP_PUT, [[{
+ "plugins": {
+ "prometheus": {
+ "prefer_name": true
+ }
+ },
+ "service_id": "svc-a"
+ }]])
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+ }
+ }
+--- response_body
+
+
+
+=== TEST 21: proxy a session through the named service
+--- stream_request
+hello
+--- stream_response
+hello world
+
+
+
+=== TEST 22: with prefer_name, service carries the name and service_id the id
+The same rule as the http metrics.
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_status\{code="200",listen_addr="0\.0\.0\.0:1985",service="Order TCP",service_id="svc-a",node="127\.0\.0\.1:1995"\} 1$/m
+
+
+
+=== TEST 23: a route with both an upstream_id and a service_id
+The service is not merged into such a route, so it is only named on the
+route itself; the http metrics still label it, and so must the stream ones.
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t("/apisix/admin/services/svc-b", ngx.HTTP_PUT, [[{
+ "name": "Billing",
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1995,
+ "weight": 1
+ }]
+ }
+ }]])
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+
+ code, body = t("/apisix/admin/upstreams/up-1", ngx.HTTP_PUT, [[{
+ "type": "roundrobin",
+ "nodes": [{
+ "host": "127.0.0.1",
+ "port": 1995,
+ "weight": 1
+ }]
+ }]])
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+
+ code, body = t("/apisix/admin/stream_routes/1", ngx.HTTP_PUT, [[{
+ "plugins": {
+ "prometheus": {
+ "prefer_name": true
+ }
+ },
+ "upstream_id": "up-1",
+ "service_id": "svc-b"
+ }]])
+ if code > 300 then
+ ngx.say(body)
+ return
+ end
+ }
+ }
+--- response_body
+
+
+
+=== TEST 24: proxy a session through that route
+--- stream_request
+hello
+--- stream_response
+hello world
+
+
+
+=== TEST 25: the session carries the service of the route
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_status\{code="200",listen_addr="0\.0\.0\.0:1985",service="Billing",service_id="svc-b",node="127\.0\.0\.1:1995"\} 1$/m
+
+
+
+=== TEST 26: the connection count carries it too
+The route has no name, so prefer_name leaves route on its id.
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_stream_connection_total\{route="1",service="Billing",service_id="svc-b"\} 1$/m
diff --git a/t/stream-plugin/prometheus.t b/t/stream-plugin/prometheus.t
index 33b037cc6..9be04a226 100644
--- a/t/stream-plugin/prometheus.t
+++ b/t/stream-plugin/prometheus.t
@@ -128,7 +128,7 @@ hello world
--- request
GET /apisix/prometheus/metrics
--- response_body eval
-qr/apisix_stream_connection_total\{route="mqtt"\} 1/
+qr/apisix_stream_connection_total\{route="mqtt",service="",service_id=""\} 1/
@@ -161,7 +161,7 @@ Received unexpected MQTT packet type+flags
--- request
GET /t
--- response_body eval
-qr/apisix_stream_connection_total\{route="mqtt"\} 2/
+qr/apisix_stream_connection_total\{route="mqtt",service="",service_id=""\} 2/