令牌使用量计量

简介

Envoy AI Gateway 会暴露遵循 OpenTelemetry GenAI 语义约定的 Prometheus 指标,其中包括每个请求的令牌使用量。通过将调用方身份作为指标标签添加,并通过平台监控栈收集该指标,你可以按部门、命名空间和模型获得统一的令牌消耗视图。同一份数据还会通过 Alauda Cost Management 用于 chargeback。

该流水线如下:网关发出令牌指标,身份作为标签附加,PodMonitor 将指标收集到平台中,MonitorDashboard 负责展示。日常查看无需编写原始 PromQL。

用例

  • 按模型展示每个部门的令牌消耗,并按项目隔离。
  • 跟踪平台中哪些模型带来了最多的令牌使用量。
  • 提供 Alauda Cost Management 用于生成 chargeback 报告的使用数据。

前提条件

  1. 已配置 llmRequestCostsAIGatewayRoute。请参见配置令牌配额。如果没有 llmRequestCosts,网关仍会发出 gen_ai_client_token_usage_token,但每个请求的令牌计数都将为零。

  2. 调用方身份已作为请求头传递。请参见认证消费者

  3. 集群已启用平台监控。可通过检查 Prometheus operator CRD 来确认:

    kubectl get crd podmonitors.monitoring.coreos.com
  4. 在接入监控之前,先确认指标确实已经发出。通过网关发送一个请求,然后读取数据平面 proxy pod 上 ExtProc sidecar 的管理端口:

    POD=$(kubectl get pod -n envoy-gateway-system \
      -l gateway.envoyproxy.io/owning-gateway-name=<gateway-name> \
      -o jsonpath='{.items[0].metadata.name}')
    kubectl port-forward -n envoy-gateway-system pod/$POD 1064:1064 &
    curl -s http://localhost:1064/metrics | grep gen_ai_client_token_usage_token | head
    # expect lines like: gen_ai_client_token_usage_token_sum{gen_ai_operation_name="chat",...} 42

    如果没有出现任何 gen_ai_* 样本,那么下面的任何抓取都不会生效——请先修复路由 / ExtProc 连接。

NOTE

请在专用 namespace 中创建 GatewayAIGatewayRoute(例如 maas-system),不要创建在 Envoy Gateway 控制平面 namespace envoy-gateway-system 中。放在控制平面 namespace 中的 gateway 可能不会将 AI Gateway 请求处理 filter 和 SecurityPolicy 应用到其 listener,这会静默破坏路由和策略执行。请参见Envoy AI Gateway

步骤

为令牌指标添加身份标签

默认情况下,令牌指标 gen_ai_client_token_usage_token 只包含 OpenTelemetry GenAI 标准标签(model、provider、operation、token type)。通过在 Envoy AI Gateway controller 中将身份 header 映射为指标标签,可将其扩展为包含调用方身份维度——计费 namespace 和调用方部门。

controller 从 ai-gateway-controller Deployment 上的 CLI flag --metricsRequestHeaderAttributes=<header>:<label>[,<header>:<label>...] 读取映射。如果 controller 是通过 Helm 安装的,则 chart 会从某个 values 键(例如 controller.metricsRequestHeaderAttributes)渲染此 flag;请提供你的 release 名称和 chart 引用,然后执行 helm upgrade --reuse-values。如果你直接管理 Deployment,则需要 patch 其容器参数。

该 flag 是一个单个以逗号连接的 <header>:<label> 映射,不是可重复列表:传入两次会导致 controller 只保留最后一份。平台还自带了默认键值对(例如 x-user-name:userx-access-meta:client_id),数据平面的 sidecar 已经会发出这些标签——因此你必须将这一个 flag 改写为现有键值对与新键值对的并集。省略任何一对,都会在下次重启时从所有 proxy 中移除该指标标签。

先读取数据平面当前实际发出的键值对(proxy sidecar 才是事实来源——controller flag 可能已经被收窄):

PROXY=$(kubectl -n envoy-gateway-system get pod \
  -l gateway.envoyproxy.io/owning-gateway-name=<gateway-name> \
  -o jsonpath='{.items[0].metadata.name}')
kubectl -n envoy-gateway-system get pod "$PROXY" \
  -o jsonpath='{.spec.initContainers[?(@.name=="ai-gateway-extproc")].args}' \
  | jq -r 'index("-metricsRequestHeaderAttributes") as $i | .[$i+1]'
# e.g. x-user-name:user,x-user-namespace:user_namespace,x-access-meta:client_id  (platform defaults)

然后用完整集合再加上 x-user-group:department 重写 controller 的单个 flag。请将下面左侧替换为上面命令实际输出的内容——不要删除任何键值对:

# Locate the flag's argument index on the controller Deployment
IDX=$(kubectl -n envoy-gateway-system get deploy ai-gateway-controller \
  -o jsonpath='{.spec.template.spec.containers[0].args}' \
  | jq 'map(test("^--?metricsRequestHeaderAttributes=")) | index(true)')

# Replace it with the UNION of existing pairs + the new one
kubectl -n envoy-gateway-system patch deploy ai-gateway-controller --type json -p '[
  {"op":"replace","path":"/spec/template/spec/containers/0/args/'"$IDX"'",
   "value":"--metricsRequestHeaderAttributes=x-user-name:user,x-user-namespace:user_namespace,x-access-meta:client_id,x-user-group:department"}
]'
# If the flag is not present anywhere (IDX is null), add it instead with the same full value:
#   --type json -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--metricsRequestHeaderAttributes=...,x-user-group:department"}]'

kubectl -n envoy-gateway-system rollout status deploy ai-gateway-controller

# The mapping is rendered into each data-plane proxy pod's ExtProc sidecar args
# at pod-creation time. A controller-only rollout does NOT update already-running
# proxy pods, so recreate them and wait:
kubectl rollout restart deployment -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=<gateway-name>
kubectl rollout status deployment -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=<gateway-name>
  • x-user-namespaceuser_namespace:请求所计费的 namespace 或 tenant。这是 通过 Cost Management 进行 chargeback 所按组使用的按 namespace 键,因此只要 Cost Management 在使用,就应保留它。
  • x-user-groupdepartment:用于 dashboard 的低基数身份维度。由 SecurityPolicy 根据 IDP groups claim 设置。
NOTE

--metricsRequestHeaderAttributes 映射会在创建 proxy pod 时被固化到 ExtProc sidecar 参数中。仅 patch controller 并等待其 rollout 不够——必须重新创建 proxy pod(上面的 rollout restart)之后,新的标签才会出现。如果下面的验证返回空输出,说明 proxy pod 没有被重新创建。

NOTE

user_namespace(计费 namespace)是可靠的默认分组——只要映射了 x-user-namespace,它就始终存在。departmentx-user-group)只有在 IDP 发出单值 group claim 时才是有用的低基数维度——标准 OIDC groups claim 是数组,不受 claimToHeaders 支持,因此在其他情况下 department 会保持为空——请参见认证消费者。避免使用按用户标签(x-user-id):它会产生高基数 series(每个用户 × 模型 × 令牌类型一条),因此仅在需要按用户报表且保留窗口能限制 series 数量时才添加它。

rollout 完成后,发送一条新的请求并确认样本上出现了新标签。user_namespace 始终存在;只有在配置了标量 group claim 时才会出现 department

curl -s http://localhost:1064/metrics | grep 'user_namespace=' | head

将指标收集到平台中

该指标由 AI Gateway external processor(ExtProc)发出,它作为每个数据平面 proxy pod 的 sidecar 运行(声明为 Kubernetes 原生 sidecar / initContainer),并在容器端口 1064(名为 aigw-metrics)上暴露 Prometheus metrics endpoint。它通过 PodMonitor 直接从 proxy pod 抓取。平台工作流请参见 指标管理

在 Alauda 平台上,此 sidecar 的 PodMonitor 往往已经预先安装。请先检查——如果在同一个 endpoint 上再创建第二个 PodMonitor,会形成重叠的 scrape pool,从而对每个 proxy pod 抓取两次,生成带有不同 job 标签的重复 series,并使 dashboard 以及 chargeback 查询中的每个 increase()/sum 翻倍(两者都不会按 job 过滤):

kubectl get podmonitor -A \
  -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\t"}{.spec.podMetricsEndpoints[*].port}{"\n"}{end}'
# The Alauda platform ships `ai-gateway-extproc-metrics` targeting the `aigw-metrics`
# port. If any PodMonitor already lists `aigw-metrics`, SKIP the apply below and go
# straight to the scrape check.

只有在没有任何 PodMonitor 已经指向 aigw-metrics 时,才去发现 Prometheus operator 用来选择 PodMonitor 对象的 label(以便下面的资源确实会被拾取),然后创建一个:

kubectl get prometheus -A \
  -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}: {.spec.podMonitorSelector}{"\n"}{end}'
# On the Alauda platform this is typically: prometheus: kube-prometheus

然后应用该 PodMonitor,并将该 label 放在其自身metadata.labels 中(不是 selector——这是两个不同的东西):

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: ai-gateway-metrics
  namespace: envoy-gateway-system
  labels:
    prometheus: kube-prometheus   # must match your Prometheus's spec.podMonitorSelector
spec:
  selector:
    matchLabels:
      app.kubernetes.io/managed-by: envoy-gateway
      app.kubernetes.io/component: proxy
  podMetricsEndpoints:
    - port: aigw-metrics
      path: /metrics
      interval: 30s
  • metadata.labels:Prometheus 通过它发现 PodMonitor。没有正确的 label,资源虽然存在,但对 scrape 流程不可见。
  • spec.selector:匹配集群中每个 Envoy Gateway 数据平面 proxy pod。若要限制到某个 Gateway,请将其替换为 gateway.envoyproxy.io/owning-gateway-name: <gateway-name>gateway.envoyproxy.io/owning-gateway-namespace: <gateway-namespace>
  • port: aigw-metrics:ExtProc sidecar 上提供 /metrics 的命名端口,对应容器端口 1064

确认 Prometheus 正在抓取该 sidecar——并且恰好只有一个 scrape pool 覆盖它(两个 pool 说明重复的 PodMonitor 导致双重计数)。对 Prometheus 做端口转发,并按 pool 对活动目标分组:

PROM=$(kubectl -n <prometheus-namespace> get pod -l app.kubernetes.io/name=prometheus \
  -o jsonpath='{.items[0].metadata.name}')
kubectl -n <prometheus-namespace> port-forward $PROM 9090:9090 &

# Wait ~30s after applying a PodMonitor; the operator's config-reload is async.
curl -s 'http://127.0.0.1:9090/api/v1/targets?state=active' \
  | jq '[.data.activeTargets[] | select(.labels.endpoint=="aigw-metrics")]
        | group_by(.scrapePool)
        | map({scrapePool: .[0].scrapePool, targets: length, health: .[0].health})'
# expect: exactly ONE scrape pool covering the sidecar, health="up".
# Two pools => a duplicate PodMonitor; delete the redundant one.

构建统一的使用量 dashboard

创建一个 MonitorDashboard 来展示令牌使用量。使用 namespace、model 和 department 变量,以便消费者可以筛选,并依赖 User View,使每个 project 只能看到自己的数据。平台工作流请参见 监控 dashboards

在 ExtProc /metrics endpoint 上,该指标以 Prometheus 文本格式暴露,OpenTelemetry 名称会规范化为下划线(如上面的前置检查所示,gen_ai_client_token_usage_token_sum)。不过,平台的 scrape 流程会以原始 OpenTelemetry UTF-8 名称存储该 series,并保留点号。因此在 Prometheus 中,该 series 为 gen_ai.client.token.usage_token_sum(以及 histogram 的 _sum/_count/_bucket 变体),必须使用带引号的 {__name__="..."} 形式来选择——裸下划线标识符将匹配不到任何内容并返回空 vector。内置的 GenAI 标签同样使用点号形式(gen_ai.request.modelgen_ai.token.type),并且需要带引号的 UTF-8 label 语法;而通过 header 映射得到的标签(例如 departmentuser_namespace)则保持普通标识符形式:

CAUTION

在首页面板中请按 user_namespace 分组:只要映射了 x-user-namespace,它就始终存在。department 标签只有x-user-group 被填充时才会出现,而这要求 IDP 提供单值 group claim——标准 OIDC groups claim 是数组,不受支持,因此否则 sum by (department) 会退化为单个空标签 series。只有在确认该标签确实存在(grep 'department=' 有输出)之后,才按 department 分组;否则,请在 IDP connector 中暴露一个标量 claim。

# Tokens per billed namespace over the last hour
sum by (user_namespace) (
  increase({__name__="gen_ai.client.token.usage_token_sum"}[1h])
)

# Top 5 models by total tokens in the last 24h
topk(5,
  sum by ("gen_ai.request.model") (
    increase({__name__="gen_ai.client.token.usage_token_sum"}[24h])
  )
)

# Output-token share per namespace (proxy for cost; output is the expensive side)
sum by (user_namespace) (
  increase({__name__="gen_ai.client.token.usage_token_sum", "gen_ai.token.type"="output"}[1h])
)

# Optional: per-department view — only once a scalar group claim is configured
# (see the caution above), otherwise this returns an empty result:
#   sum by (department) (increase({__name__="gen_ai.client.token.usage_token_sum"}[1h]))

通过在 Prometheus UI 的 Status → TSDB 页面列出所有 gen_ai* series 名称,或使用以下命令,来确认该指标可查询:

curl -s 'http://127.0.0.1:9090/api/v1/label/__name__/values' \
  | jq -r '.data[]' | grep gen_ai

验证

发送几条已认证且会解析到不同 namespace 的请求,然后通过对任意 proxy pod 的 ExtProc sidecar metrics 端口做端口转发,确认该指标携带了身份标签:

POD=$(kubectl get pod -n envoy-gateway-system \
  -l gateway.envoyproxy.io/owning-gateway-name=<gateway-name> \
  -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward -n envoy-gateway-system pod/$POD 1064:1064 &

# Drive a couple of authenticated requests (substitute valid tokens), then read the metric.
for token in '<token-tenant-a>' '<token-tenant-b>'; do
  curl -s -o /dev/null \
    -H "Authorization: Bearer $token" \
    -H 'Content-Type: application/json' \
    -d '{"model":"my-llm","messages":[{"role":"user","content":"hi"}]}' \
    http://<gateway-address>/v1/chat/completions
done

curl -s http://localhost:1064/metrics \
  | grep 'gen_ai_client_token_usage_token_sum{' \
  | grep 'user_namespace='

预期每个计费 namespace 至少有一个样本,例如:

gen_ai_client_token_usage_token_sum{user_namespace="team-a",gen_ai_request_model="my-llm",gen_ai_token_type="input",...} 184
gen_ai_client_token_usage_token_sum{user_namespace="team-b",gen_ai_request_model="my-llm",gen_ai_token_type="input",...} 91

如果你配置了标量 group claim,这些样本还会带有 department= 标签(可通过 grep 'department=' 检查)。打开 dashboard,确认令牌使用量已经显示,并且可以按 namespace、model 和 department 进行筛选。

了解更多

下一步

在收集到令牌使用量之后,配置对令牌使用量进行费用分摊,将该指标通过 Alauda Cost Management 计价并生成按 namespace 计费的账单。