This is an automated email from the ASF dual-hosted git repository.
shreemaan-abhishek 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 f6d0ee804 fix(prometheus): truncate llm model-name labels to bound
cardinality (#13637)
f6d0ee804 is described below
commit f6d0ee8040ccaf5da9f3f3b05a6ede8a86a66a9a
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Thu Jul 2 18:16:32 2026 +0800
fix(prometheus): truncate llm model-name labels to bound cardinality
(#13637)
---
apisix/plugins/prometheus/exporter.lua | 54 +++++++++++++++++++-------
docs/en/latest/plugins/prometheus.md | 2 +
docs/zh/latest/plugins/prometheus.md | 2 +
t/plugin/prometheus-ai-proxy.t | 71 ++++++++++++++++++++++++++++++++++
4 files changed, 116 insertions(+), 13 deletions(-)
diff --git a/apisix/plugins/prometheus/exporter.lua
b/apisix/plugins/prometheus/exporter.lua
index 90c5d810d..de3e4a8bf 100644
--- a/apisix/plugins/prometheus/exporter.lua
+++ b/apisix/plugins/prometheus/exporter.lua
@@ -27,6 +27,7 @@ local C = ffi.C
local pcall = pcall
local select = select
local type = type
+local tostring = tostring
local prometheus
local prometheus_bkp
local router = require("apisix.router")
@@ -45,6 +46,7 @@ local latency_details =
require("apisix.utils.log-util").latency_details_in_ms
local xrpc = require("apisix.stream.xrpc")
local unpack = unpack
local next = next
+local str_sub = string.sub
local process = require("ngx.process")
local tonumber = tonumber
local shdict_prometheus_cache = ngx.shared["prometheus-cache"]
@@ -64,6 +66,25 @@ local DEFAULT_BUCKETS = {1, 2, 5, 10, 20, 50, 100, 200, 500,
1000, 2000, 5000, 1
-- sizes) with the upper bound raised to 1M to cover large-context models.
local DEFAULT_TOKEN_BUCKETS = {1, 10, 50, 100, 200, 500, 1000, 2000, 5000,
10000,
20000, 50000, 100000, 200000, 500000, 1000000}
+
+-- Max byte length for model-name labels. Model names are client-supplied, so
+-- cap them to stop arbitrarily long strings from exhausting the metrics shm.
+-- Non-scalar values become "<non-scalar>" so pointer strings never enter the
index.
+local MAX_MODEL_LABEL_LEN = 128
+
+local function model_to_label(val)
+ if val == nil then
+ return nil
+ end
+ local t = type(val)
+ if t == "string" then
+ return str_sub(val, 1, MAX_MODEL_LABEL_LEN)
+ elseif t == "number" then
+ return str_sub(tostring(val), 1, MAX_MODEL_LABEL_LEN)
+ else
+ return "<non-scalar>"
+ end
+end
-- Default refresh interval
local DEFAULT_REFRESH_INTERVAL = 15
@@ -435,11 +456,17 @@ function _M.http_log(conf, ctx)
local response_source = core.response.get_response_source(ctx)
+ -- Truncate model names before they become label values.
vars.request_llm_model /
+ -- vars.llm_model keep their full values for other consumers; only the
metrics path
+ -- is capped.
+ local request_llm_model_label = model_to_label(vars.request_llm_model)
+ local llm_model_label = model_to_label(vars.llm_model)
+
metrics.status:inc(1,
get_enabled_label_values_for_metric("http_status",
disabled_label_metric_map,
vars.status, route_id, matched_uri, matched_host,
service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
response_source,
unpack(extra_labels("http_status", ctx))))
@@ -450,21 +477,21 @@ function _M.http_log(conf, ctx)
metrics.latency:observe(latency,
get_enabled_label_values_for_metric("http_latency",
disabled_label_metric_map,
"request", route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(latency_extra_label_values)))
if upstream_latency then
metrics.latency:observe(upstream_latency,
get_enabled_label_values_for_metric("http_latency",
disabled_label_metric_map,
"upstream", route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(latency_extra_label_values)))
end
metrics.latency:observe(apisix_latency,
get_enabled_label_values_for_metric("http_latency",
disabled_label_metric_map,
"apisix", route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(latency_extra_label_values)))
local bandwidth_extra_label_values = extra_labels("bandwidth", ctx)
@@ -472,13 +499,13 @@ function _M.http_log(conf, ctx)
metrics.bandwidth:inc(vars.request_length,
get_enabled_label_values_for_metric("bandwidth",
disabled_label_metric_map,
"ingress", route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(bandwidth_extra_label_values)))
metrics.bandwidth:inc(vars.bytes_sent,
get_enabled_label_values_for_metric("bandwidth",
disabled_label_metric_map,
"egress", route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(bandwidth_extra_label_values)))
if vars.request_type == "ai_stream" or vars.request_type == "ai_chat" then
@@ -491,7 +518,7 @@ function _M.http_log(conf, ctx)
metrics.llm_latency:observe(tonumber(vars.apisix_upstream_response_time),
get_enabled_label_values_for_metric("llm_latency",
disabled_label_metric_map,
"total", route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label,
llm_model_label,
unpack(extra_labels("llm_latency", ctx))))
-- type="ttft": time to first token, only streaming exposes a real
one.
@@ -499,7 +526,7 @@ function _M.http_log(conf, ctx)
metrics.llm_latency:observe(tonumber(llm_time_to_first_token),
get_enabled_label_values_for_metric("llm_latency",
disabled_label_metric_map,
"ttft", route_id, service_id, consumer_name,
balancer_ip,
- vars.request_type, vars.request_llm_model,
vars.llm_model,
+ vars.request_type, request_llm_model_label,
llm_model_label,
unpack(extra_labels("llm_latency", ctx))))
end
end
@@ -507,27 +534,27 @@ function _M.http_log(conf, ctx)
metrics.llm_prompt_tokens:inc(tonumber(vars.llm_prompt_tokens),
get_enabled_label_values_for_metric("llm_prompt_tokens",
disabled_label_metric_map,
route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(extra_labels("llm_prompt_tokens", ctx))))
metrics.llm_prompt_tokens_dist:observe(tonumber(vars.llm_prompt_tokens),
get_enabled_label_values_for_metric("llm_prompt_tokens_dist",
disabled_label_metric_map,
route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(extra_labels("llm_prompt_tokens_dist", ctx))))
metrics.llm_completion_tokens:inc(tonumber(vars.llm_completion_tokens),
get_enabled_label_values_for_metric("llm_completion_tokens",
disabled_label_metric_map,
route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(extra_labels("llm_completion_tokens", ctx))))
metrics.llm_completion_tokens_dist:observe(tonumber(vars.llm_completion_tokens),
get_enabled_label_values_for_metric("llm_completion_tokens_dist",
disabled_label_metric_map,
route_id, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, request_llm_model_label, llm_model_label,
unpack(extra_labels("llm_completion_tokens_dist", ctx))))
end
end
@@ -931,7 +958,8 @@ local function inc_llm_active_connections(ctx, value)
get_enabled_label_values_for_metric("llm_active_connections",
disabled_label_metric_map,
route_name, route_id, matched_uri,
matched_host, service_name, service_id, consumer_name, balancer_ip,
- vars.request_type, vars.request_llm_model, vars.llm_model,
+ vars.request_type, model_to_label(vars.request_llm_model),
+ model_to_label(vars.llm_model),
unpack(extra_labels("llm_active_connections", ctx)))
)
end
diff --git a/docs/en/latest/plugins/prometheus.md
b/docs/en/latest/plugins/prometheus.md
index 194d32042..e31ff269e 100644
--- a/docs/en/latest/plugins/prometheus.md
+++ b/docs/en/latest/plugins/prometheus.md
@@ -106,6 +106,8 @@ Collapsing a label's value to `""` keeps the label
registered in the metric sche
See [Reduce Metric Cardinality by Disabling
Labels](#reduce-metric-cardinality-by-disabling-labels) for an example.
+The `request_llm_model` and `llm_model` label values are derived from
client-supplied model names. To bound cardinality, APISIX truncates each of
these label values to 128 bytes before recording. If you do not need per-model
breakdowns, list `request_llm_model` and `llm_model` under `disabled_labels`
for the LLM metrics to collapse them to a single empty-valued series.
+
## Metrics
There are different types of metrics in Prometheus. To understand their
differences, see [metrics
types](https://prometheus.io/docs/concepts/metric_types/).
diff --git a/docs/zh/latest/plugins/prometheus.md
b/docs/zh/latest/plugins/prometheus.md
index d7eed001a..f647b5d6a 100644
--- a/docs/zh/latest/plugins/prometheus.md
+++ b/docs/zh/latest/plugins/prometheus.md
@@ -106,6 +106,8 @@ plugin_attr:
示例请参见[通过禁用标签降低指标基数](#通过禁用标签降低指标基数)。
+`request_llm_model` 与 `llm_model` 标签值来源于客户端提供的模型名称。为了限制基数,APISIX
在记录前会将这两个标签值截断为 128 字节。如果你不需要按模型细分,可将 `request_llm_model` 和 `llm_model` 列入 LLM
指标的 `disabled_labels`,从而将其折叠为一条空值时间序列。
+
## 指标
Prometheus
中有不同类型的指标。要了解它们之间的区别,请参见[指标类型](https://prometheus.io/docs/concepts/metric_types/)。
diff --git a/t/plugin/prometheus-ai-proxy.t b/t/plugin/prometheus-ai-proxy.t
index 9a641825b..0e58f1c2f 100644
--- a/t/plugin/prometheus-ai-proxy.t
+++ b/t/plugin/prometheus-ai-proxy.t
@@ -432,3 +432,74 @@
qr/apisix_llm_latency_bucket\{type="ttft",.*route_id="4",.*,node="openai-gpt4".*
GET /apisix/prometheus/metrics
--- response_body eval
qr/apisix_llm_latency_count\{type="total",.*route_id="4",.*,node="openai-gpt4".*request_type="ai_stream",request_llm_model="gpt-3",llm_model="gpt-4"\}
1/
+
+
+
+=== TEST 21: send a chat request whose model name exceeds the 128-byte label
cap
+--- request eval
+"POST /chat\n" .
+qq#{"messages":[{"role":"user","content":"What is 1+1?"}], "model": "@{[ 'a' x
200 ]}"}#
+--- more_headers
+X-AI-Fixture: prometheus/chat-basic.json
+--- error_code: 200
+
+
+
+=== TEST 22: request_llm_model label is truncated to 128 bytes (cardinality
DoS guard)
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_llm_prompt_tokens\{.*request_llm_model="a{128}",llm_model="gpt-4"\}/
+--- response_body_unlike eval
+qr/request_llm_model="a{129}"/
+
+
+
+=== TEST 23: disable request_llm_model / llm_model labels via plugin_metadata
disabled_labels
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local _, body = t("/apisix/admin/plugin_metadata/prometheus",
+ ngx.HTTP_PUT,
+ [[{
+ "disabled_labels": {
+ "llm_prompt_tokens": ["request_llm_model", "llm_model"]
+ }
+ }]]
+ )
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 24: send a chat request with a distinct model
+--- request
+POST /chat
+{"messages":[{"role":"user","content":"What is 1+1?"}], "model":
"distinct-model-aaa"}
+--- more_headers
+X-AI-Fixture: prometheus/chat-basic.json
+--- error_code: 200
+
+
+
+=== TEST 25: send another chat request with a different distinct model
+--- request
+POST /chat
+{"messages":[{"role":"user","content":"What is 1+1?"}], "model":
"distinct-model-bbb"}
+--- more_headers
+X-AI-Fixture: prometheus/chat-basic.json
+--- error_code: 200
+
+
+
+=== TEST 26: disabled_labels collapses distinct client models to one
empty-valued series
+--- request
+GET /apisix/prometheus/metrics
+--- response_body_like eval
+qr/apisix_llm_prompt_tokens\{.*request_llm_model="",llm_model=""\}/
+--- response_body_unlike eval
+qr/apisix_llm_prompt_tokens\{.*request_llm_model="distinct-model-/