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 494dc4229 feat: label WebSocket sessions with request_type=websocket 
(#13909)
494dc4229 is described below

commit 494dc42299d3bd9e9edc5be13c713a5946d525a1
Author: Mohammad Izzraff Janius 
<[email protected]>
AuthorDate: Thu Sep 3 15:24:51 2026 +0900

    feat: label WebSocket sessions with request_type=websocket (#13909)
---
 apisix/init.lua                      |   4 +
 docs/en/latest/plugins/prometheus.md |   6 +-
 docs/zh/latest/plugins/prometheus.md |   6 +-
 t/plugin/prometheus-websocket.t      | 183 +++++++++++++++++++++++++++++++++++
 4 files changed, 193 insertions(+), 6 deletions(-)

diff --git a/apisix/init.lua b/apisix/init.lua
index 7e6675850..8953eba1b 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -1024,6 +1024,10 @@ function _M.http_header_filter_phase()
         set_resp_upstream_status(up_status)
     end
 
+    if ngx.status == 101 then
+        ngx_var.request_type = "websocket"
+    end
+
     common_phase("header_filter")
 
     local api_ctx = ngx.ctx.api_ctx
diff --git a/docs/en/latest/plugins/prometheus.md 
b/docs/en/latest/plugins/prometheus.md
index a26787642..95d89f02f 100644
--- a/docs/en/latest/plugins/prometheus.md
+++ b/docs/en/latest/plugins/prometheus.md
@@ -197,7 +197,7 @@ The following labels are used to differentiate 
`apisix_http_status` metrics.
 | service      | ID of the Service that the HTTP status originates from when 
`prefer_name` is `false` (default), and name of the Service when `prefer_name` 
to `true`. Default to the configured value of host on the Route if the matched 
Route does not belong to any Service. |
 | consumer     | Name of the Consumer associated with a request. Default to an 
empty string if no Consumer is associated with the request.                     
  |
 | node         | IP address of the Upstream node. For AI Routes, this is the 
LLM instance name reported by `ai-proxy` or `ai-proxy-multi`. |
-| request_type       | Request category: `traditional_http`, `ai_chat`, or 
`ai_stream`.                                                               |
+| request_type       | Request category: `traditional_http`, `websocket`, 
`ai_chat`, or `ai_stream`. A request answered with `101 Switching Protocols` is 
`websocket`. |
 | request_llm_model  | Model name requested by the client.                     
                                                                       |
 | 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. |
@@ -257,7 +257,7 @@ The following labels are used to differentiate 
`apisix_bandwidth` metrics.
 | service    | ID of the Service that bandwidth corresponds to when 
`prefer_name` is `false` (default), and name of the Service when `prefer_name` 
to `true`. Default to the configured value of host on the Route if the matched 
Route does not belong to any Service. |
 | consumer   | Name of the Consumer associated with a request. Default to an 
empty string if no Consumer is associated with the request.                     
  |
 | node       | IP address of the Upstream node. For AI Routes, this is the LLM 
instance name reported by `ai-proxy` or `ai-proxy-multi`. |
-| request_type       | Request category: `traditional_http`, `ai_chat`, or 
`ai_stream`.                                                               |
+| request_type       | Request category: `traditional_http`, `websocket`, 
`ai_chat`, or `ai_stream`. A request answered with `101 Switching Protocols` is 
`websocket`. |
 | request_llm_model  | Model name requested by the client.                     
                                                                       |
 | 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. |
 
@@ -386,7 +386,7 @@ The following labels are used to differentiate 
`apisix_http_latency` metrics.
 | service    | ID of the Service that latencies correspond to when 
`prefer_name` is `false` (default), and name of the Service when `prefer_name` 
to `true`. Default to the configured value of host on the Route if the matched 
Route does not belong to any Service. |
 | consumer   | Name of the Consumer associated with latencies. Default to an 
empty string if no Consumer is associated with the request.                     
        |
 | node       | IP address of the Upstream node associated with latencies. For 
AI Routes, this is the LLM instance name reported by `ai-proxy` or 
`ai-proxy-multi`. |
-| request_type       | Request category: `traditional_http`, `ai_chat`, or 
`ai_stream`.                                                               |
+| request_type       | Request category: `traditional_http`, `websocket`, 
`ai_chat`, or `ai_stream`. A request answered with `101 Switching Protocols` is 
`websocket`. |
 | request_llm_model  | Model name requested by the client.                     
                                                                       |
 | 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. |
 
diff --git a/docs/zh/latest/plugins/prometheus.md 
b/docs/zh/latest/plugins/prometheus.md
index 48778dab4..86081f9ff 100644
--- a/docs/zh/latest/plugins/prometheus.md
+++ b/docs/zh/latest/plugins/prometheus.md
@@ -197,7 +197,7 @@ Prometheus 中有不同类型的指标。要了解它们之间的区别,请参
 | service | HTTP 状态来源的服务 ID,当 `prefer_name` 为 `false`(默认)时,使用服务 ID,当 
`prefer_name` 为 `true` 时,使用服务名称。如果匹配的路由不属于任何服务,则默认为路由上配置的主机值。 |
 | consumer | 与请求关联的消费者名称。如果请求没有与之关联的消费者,则默认为空字符串。                              
               |
 | node   | 上游节点的 IP 地址。对于 AI 路由,该值为 `ai-proxy` 或 `ai-proxy-multi` 上报的 LLM 
实例名称。                           |
-| request_type       | 请求类别:`traditional_http`、`ai_chat` 或 `ai_stream`。        
                                                                 |
+| request_type       | 请求类别:`traditional_http`、`websocket`、`ai_chat` 或 
`ai_stream`。以 `101 Switching Protocols` 响应的请求为 `websocket`。 |
 | request_llm_model  | 客户端请求的模型名称。                                             
                                                              |
 | llm_model          | AI 请求实际使用的目标模型。优先使用 AI 实例中配置的模型,否则使用客户端请求的模型;传统 HTTP 
流量中为空字符串。 |
 | response_source    | 响应来源:`apisix` 表示由 APISIX 生成,`nginx` 表示 NGINX 
代理错误,`upstream` 表示来自上游服务的响应。                              |
@@ -245,7 +245,7 @@ UDP 没有关闭、FIN 或重置信号,因此只会出现其中一部分状态
 | service | 请求对应的服务 ID,当 `prefer_name` 为 `false`(默认)时,使用服务 ID,当 `prefer_name` 
为 `true` 时,使用服务名称。如果匹配的路由不属于任何服务,则默认为路由上配置的主机值。 |
 | consumer | 与请求关联的消费者名称。如果请求没有与之关联的消费者,则默认为空字符串。                              
               |
 | node   | 上游节点的 IP 地址。对于 AI 路由,该值为 `ai-proxy` 或 `ai-proxy-multi` 上报的 LLM 
实例名称。                           |
-| request_type       | 请求类别:`traditional_http`、`ai_chat` 或 `ai_stream`。        
                                                                 |
+| request_type       | 请求类别:`traditional_http`、`websocket`、`ai_chat` 或 
`ai_stream`。以 `101 Switching Protocols` 响应的请求为 `websocket`。 |
 | request_llm_model  | 客户端请求的模型名称。                                             
                                                              |
 | llm_model          | AI 请求实际使用的目标模型。优先使用 AI 实例中配置的模型,否则使用客户端请求的模型;传统 HTTP 
流量中为空字符串。 |
 
@@ -374,7 +374,7 @@ APISIX 在开始 LLM 上游尝试时递增该 gauge,并在请求的日志阶
 | service | 延迟对应的服务 ID,当 `prefer_name` 为 `false`(默认)时,使用服务 ID,当 `prefer_name` 
为 `true` 时,使用服务名称。如果匹配的路由不属于任何服务,则默认为路由上配置的主机值。 |
 | consumer | 与延迟关联的消费者名称。如果请求没有与之关联的消费者,则默认为空字符串。                              
               |
 | node   | 与延迟关联的上游节点 IP 地址。对于 AI 路由,该值为 `ai-proxy` 或 `ai-proxy-multi` 上报的 LLM 
实例名称。               |
-| request_type       | 请求类别:`traditional_http`、`ai_chat` 或 `ai_stream`。        
                                                                 |
+| request_type       | 请求类别:`traditional_http`、`websocket`、`ai_chat` 或 
`ai_stream`。以 `101 Switching Protocols` 响应的请求为 `websocket`。 |
 | request_llm_model  | 客户端请求的模型名称。                                             
                                                              |
 | llm_model          | AI 请求实际使用的目标模型。优先使用 AI 实例中配置的模型,否则使用客户端请求的模型;传统 HTTP 
流量中为空字符串。 |
 
diff --git a/t/plugin/prometheus-websocket.t b/t/plugin/prometheus-websocket.t
new file mode 100644
index 000000000..b82d4fe98
--- /dev/null
+++ b/t/plugin/prometheus-websocket.t
@@ -0,0 +1,183 @@
+#
+# 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) = @_;
+
+    if (!defined $block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    if (!defined $block->yaml_config) {
+        $block->set_value("yaml_config", <<'EOC');
+plugin_attr:
+    prometheus:
+        refresh_interval: 0.1
+EOC
+    }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set up the metrics route and a websocket-enabled route that logs 
$request_type
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+
+            local code = t('/apisix/admin/routes/metrics',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {"public-api": {}},
+                    "uri": "/apisix/prometheus/metrics"
+                }]])
+            if code >= 300 then
+                ngx.status = code
+                ngx.say("failed to create metrics route")
+                return
+            end
+
+            local code, body = t('/apisix/admin/routes/ws',
+                ngx.HTTP_PUT,
+                [[{
+                    "plugins": {
+                        "prometheus": {},
+                        "serverless-post-function": {
+                            "phase": "log",
+                            "functions": [
+                                "return function(conf, ctx) ngx.log(ngx.WARN, 
\"request_type=\", ngx.var.request_type) end"
+                            ]
+                        }
+                    },
+                    "enable_websocket": true,
+                    "upstream": {
+                        "nodes": {"127.0.0.1:1980": 1},
+                        "type": "roundrobin"
+                    },
+                    "uri": "/websocket_handshake"
+                }]])
+            if code >= 300 then
+                ngx.status = code
+                ngx.say(body)
+                return
+            end
+
+            ngx.sleep(0.5)
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 2: a completed upgrade is logged as request_type=websocket
+--- config
+    location /t {
+        content_by_lua_block {
+            local client = require("resty.websocket.client")
+            local wb = client:new()
+            local ok, err = 
wb:connect("ws://127.0.0.1:1984/websocket_handshake")
+            if not ok then
+                ngx.say("failed to connect: ", err)
+                return
+            end
+
+            local data, typ, err = wb:recv_frame()
+            if not data then
+                ngx.say("failed to receive frame: ", err)
+                return
+            end
+
+            wb:close()
+            ngx.say("received: ", data, " (", typ, ")")
+        }
+    }
+--- response_body
+received: hello (text)
+--- error_log
+request_type=websocket
+
+
+
+=== TEST 3: the session's status is labelled request_type=websocket
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_http_status\{code="101",route="ws",[^}]*request_type="websocket"[^}]*\}
 1\n/
+
+
+
+=== TEST 4: the session's latency is labelled request_type=websocket
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_http_latency_count\{type="request",route="ws",[^}]*request_type="websocket"[^}]*\}
 1\n/
+
+
+
+=== TEST 5: the session's bandwidth is labelled request_type=websocket
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_bandwidth\{type="ingress",route="ws",[^}]*request_type="websocket"[^}]*\}
 \d+\n/
+
+
+
+=== TEST 6: a refused handshake on the same route stays 
request_type=traditional_http
+--- request
+GET /websocket_handshake
+--- error_code: 400
+--- error_log
+failed to new websocket: bad "upgrade" request header
+request_type=traditional_http
+
+
+
+=== TEST 7: the refused request lands in its own series
+--- request
+GET /apisix/prometheus/metrics
+--- response_body eval
+qr/apisix_http_status\{code="400",route="ws",[^}]*request_type="traditional_http"[^}]*\}
 1\n/
+
+
+
+=== TEST 8: the websocket series is untouched by the refused request
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_http_latency_count\{type="request",route="ws",[^}]*request_type="websocket"[^}]*\}
 1\n/
+--- response_body_unlike eval
+qr/apisix_http_latency_count\{type="request",route="ws",[^}]*request_type="websocket"[^}]*\}
 2\n/

Reply via email to