指标与用量事件
AISIX AI 网关会公开聚合指标和可导出的用量事件。这些信号共同反映服务健康状况、流量趋势,以及每次请求尝试对应的模型、路由和 策略结果。
选择遥测来源
请先选择与运维问题匹配的来源;需要深入调查问题时,再按请求、模型或服务提供方关联各类信号。
| 运维问题 | 首选来源 | 提供的信息 |
|---|---|---|
| 各网关实例的流量是否健康? | Prometheus 指标 | 请求速率、延迟分布、Token 和成本计数、策略结果、路由健康、缓存行为和导出器投递健康。 |
| 单个请求发生了什么? | 访问日志 | 结构化请求字段,包括状态、延迟、模型、模型服务提供方、请求 ID,以及可用时的路由结果。 |
| 调用应用可以观察到什么? | 响应头 | 受支持路由上的请求关联、缓存结果、重试时机和选中目标提示。 |
| 请求记录可以存储到哪里、如何分析或用于核算? | 用量事件 | 通过可观测性导出器投递的逐次尝试结果和消耗记录。 |
抓取 Prometheus 指标
AISIX 默认通过专用指标监听器在 /metrics 提供 Prometheus 指标。你可以通过启动期可观测性设置修改路径或禁用该端点。
该端点设计上不做认证。请保持专用指标监听器私有。
在启动配置中配置 Prometheus 暴露:
observability:
metrics:
prometheus:
enabled: true
path: "/metrics"
专用监听器默认绑定到 0.0.0.0:9090。如果 Prometheus 应从其他接口或端口抓取指标,请设置不同的监听地址 。
抓取默认指标端点:
curl -sS "http://127.0.0.1:9090/metrics"
AISIX 会在每次抓取时发布配置状态。其他指标族会在首次观测时注册,因此流量指标可能不会在启动后立即出现。请先发送一次模型请求,再检查 aisix_requests_total 和 aisix_tokens_consumed_total 等序列。
AISIX 使用 aisix_ 前缀输出原生指标名称。使用直方图序列计算跨网关实例的延迟百分位数,并使用请求计数器分析成功率和路由情况。精确指标名称、标签范围和 PromQL 示例请参见指标参考。
通过 observability.metrics.labels 可以选择各指标族输出的标签。配置示例、默认值和完整变量列表参见指标标签与变量。
区分请求计数与尝试计数
请求是一次调用方交互。重试或故障转移可能让一个请求产生多次用量事件尝试。部分尝试会在目标级限流或请求组装阶段停止,尚未调用模型服务提供方,因此 AISIX 在不同信号中对尝试的计量方式也不同:
| 单位 | 统计位置 | 一个样本或一行代表什么 |
|---|---|---|
| 请求 | aisix_proxy_requests_total、aisix_llm_requests_total | 一次客户端请求,状态是调用方实际收到的状态码。 |
| 尝试 | aisix_deployment_requests_total 及部署指标族 | 对某一个目标模型的一次上游调用。 |
| 发出尝试 | aisix_usage_events_emitted_total | 一次用量事件的发出尝试,在投递队列接受或拒绝它之前计数。 |
| 尝试 | 用量事件记录,以及基于其构建的用量日志 | 一次已记录的处理尝试,包括在上游调用前停止的尝试。同一请求的各次尝试共享 request_id,并按 attempt_index 排序。 |
如果某个目标返回 502,而后备目标成功,请求计数器只记录最终返回给调用方的 200。部署计数器和用量事件则会保留失败的尝试。因此,用量日志中的 5xx 尝试数可能远高于请求指标;两者计量的单位不同。
请按各查询真正回答的问题选用:
# 最终以服务端错误结束的请求——调用方实际经历的情况。
sum(increase(aisix_proxy_requests_total{status=~"5.."}[24h]))
# 发生过故障转移、但最终仍以服务 端错误结束的请求。
sum(increase(aisix_proxy_requests_total{status=~"5..", is_fallback="true"}[24h]))
# 失败的上游尝试,按目标聚合——包含后续被故障转移救回的那些。
# 这是 5xx 用量日志行数在尝试层面的对应视图,范围限于通过模型组下发的端点。
sum(increase(aisix_deployment_failure_responses_total[24h])) by (model)
# 成功救回请求的故障转移,按模型组和实际到达的目标聚合。
sum(increase(aisix_routing_successful_fallbacks_total[24h])) by (model, fallback_model)
# 用量事件的发出尝试,以及被交接队列拒绝的事件。
sum(increase(aisix_usage_events_emitted_total{status_code="5xx"}[24h]))
sum(increase(aisix_usage_event_drops_total[24h]))
# 某个成员被限流拒绝的次数。`status` 携带原始 HTTP 状态码,
# 因此无需扫描整个状态族即可定位单一故障模式。
sum(increase(aisix_usage_events_emitted_total{user_id="<member-id>", status="429"}[24h]))
# 哪些队列交接被拒绝了。队列侧的 reason 与发出计数器携带相同的模型与
# Provider Key 标签,因此该差值在按模型的粒度上同样成立,而不只是总量。
sum(increase(aisix_usage_event_drops_total{reason=~"sink_.*"}[24h])) by (model, provider_key_name)
# 网关已记录但未能投递到控制面的用量。这些样本只携带成员标签,
# 因此按成员分组,而不是按模型分组。
sum(increase(aisix_usage_event_drops_total{reason=~"send_failed|retry_budget_exhausted"}[24h])) by (reason, user_name)
比较相同的数据范围
在将两个总数视为不一致之前,请先检查它们的覆盖范围:
| 差异 | 原因 | 检查方法 |
|---|---|---|
| 抓取覆盖范围 | 请求计数器不带环境标签。只有 Prometheus 抓取了服务该环境的每个网关,单环境用量总数才可与之比较。 | 按 job 和 instance 对原始计数器分组,再与正在运行的网关实例核对。 |
| 时间覆盖范围 | increase(...[24h]) 只使用该区间内存在的样本。重启或更短的 Prometheus 保留期可能造成用量日志查询中没有的缺口。 | 在相同时间范围内绘制原始计数器。 |
| 投递覆盖范围 | aisix_usage_event_drops_total 既统计因接收端被禁用、队列已满或关闭而无法进入投递队列的事件(sink_disabled、sink_full、sink_closed),也统 计工作线程已接受、但未能投递到控制面的事件(send_failed、retry_budget_exhausted)。导出器侧的丢失不计入该指标。 | 队列侧丢弃按 model 分组;投递侧丢弃按 user_name 分组——它们不带模型和 Provider Key 归因。监控控制面投递的 telemetry batch failed (events dropped) 和导出器投递的 sink delivery dropped。控制面在已接受的批次中拒绝的事件则由 aisix_usage_events_rejected_total 统计。 |
| 没有上游调用 | 目标级限流和请求组装失败仍会产生用量事件,但没有部署计数器样本。组装失败包括凭证不可用、缺少 model_name,以及 api_base 缺失或格式错误。 | 配置错误的目标会产生失败请求,但没有部署失败,因为模型服务提供方从未被调用。 |
部署指标族覆盖通过模型组分发的端点,而用量事件也覆盖直连模型。请逐项隔离差异,不要将全部差值都解释为网关侧拒绝。
导出用量事件
用量事件是受支持代理路径输出的逐次尝试记录。发生重试或故障转移的请求会生成多条具有相同 request_id 的事件,并按 attempt_index 排序。因此统计这些记录得到的是尝试数而非请求数——在拿记录条数与请求指标作比较之前,请先阅读区分请求计数与尝试计数。/v1/messages/count_tokens 以及支持的端点中列出的单次调用端点遵循同样的规则:每次导致重试或故障转移的失败尝试都会产生一条自己的零 Token 事件。
每条事件都携带 occurred_at,即网关记录该事件的时间,格式为带恰好三位小数的 RFC 3339 UTC 时间戳,例如 2026-09-24T12:42:25.123Z。对象存储、阿里云 SLS 和 Datadog 按原样接收该字符串,OTLP 导出器则在其纳秒时间戳中携带同一时间。因此,同一请求的各次尝试,以及在同一秒内完成的事件,都可以精确到毫秒排序。RFC 3339 解析器都接受带小数的形式;只接受整秒(…:SSZ)的解析器需要更新。
用量事件不能从本地端点读取。请在可观测性导出器中配置导出器,将其投递到 OTLP/HTTP、对象存储、阿里云 SLS 或 Datadog。
每条事件都会包含请求结果、消耗详情、调用方请求的模型别名,以及网关能够观测到时处理该次尝试的解析模型。在 /v1/chat/completions 上,每条事件还会携带 cache_status(disabled、miss、hit 或 bypass);由响应缓存返回的响应会把命中层记录在 cache_hit_layer(exact 或 semantic),语义命中还会把匹配相似度记录在 cache_similarity。流式响应从不被缓存,因此即使环境中启用了缓存策略,流式请求的事件也会上报 cache_status="disabled"。
延迟字段会区分服务提供方耗时和调用方可见耗时:
| 字段 | 范围 |
|---|---|
upstream_latency_ms | 一次上游尝试所花费的时间,不包括请求解析、安全护栏、路由、重试延迟和先前尝试。 |
upstream_ttft_ms | 从一次上游尝试开始到首个流式帧的时间,无论帧类型为何——包括 response.created、仅含角色的 chat 增量等元数据起始帧,与调用方侧代理的统计口径一致。非流式请求、错误和缓存命中时省略或为零。 |
downstream_latency_ms | 从收到请求到向下游投递的时间:非流式响应为完整响应,流式响应为首个 Token 或转发帧,A2A 和 /v1/audio/speech 则为整个流。它包括网关处理、重试及重试延迟和输出暂存,仅出现在最后一次尝试中。 |
如果调用方放弃请求,仍会生成用量事件,因为上游可能已经执行工作并计费。这些事件的状态码为 499、error_class 为 client_disconnected 而不是 200,因此可以作为一个类别筛选;流式传输中途断开时,只统计断开前已到达的 Token。统计成功请求时请使用 status_code = 200,以排除被放弃的请求。
用量日志记录了什么
用量事件是请求的记录,而不是计费条目。只要 AISIX 能够将请求归因到某个调用方,就会为其写入用量事件,包括从未到达模型服务提供方的请求,以及没有任何可计费内容的请求(这类请求记录为零 Token)。可以用下表区分「没有记录」和「记录了一次拒绝」:
| 结果 | 是否记录 | 记录内容 |
|---|---|---|
| 成功 | 是 | 状态 200、解析出的目标,以及上报的消耗量。 |
| Rerank 成功但没有 Token 数 | 是 | 状态 200,零 Token。例如 Cohere 的 rerank 模型上报的是搜索单元而不是 Token,AISIX 不对搜索单元定价。 |
| 模型服务提供方不支持该端点 | 是 | 状态 501,零 Token,没有上游调用。当解析出的模型服务提供方缺少对应能力时,AISIX 会在文本补全、Embeddings、图像生成和视频提交上自行返回该状态。 |
| 缓存命中 | 是 | 状态 200,回放所存响应的 Token 数量,成本为零,cache_status="hit",cache_hit_layer 为 exact 或 semantic,语义命中还带 cache_similarity。由于没有联系任何上游,它不携带 provider_request_id,也没有路由目标,但 provider_model_version 会标明产出所存响应的那个模型。只有非流式请求会被缓存,因此缓存命中与流式响应不会同时出现。 |
| 模型未找到 | 是 | 状态 404、error_class="model_not_found",model_id 为空。 |
| 被限流拒绝 | 是 | 状态 429、error_class="rate_limit_exceeded"。调用方 Key 级、模型级和限流策略的拒绝都是这一形态,三者都在下发前完成检查。 |
| 预算耗尽 | 是 | 状态 429、error_class="billing_error"。 |
| 安全护栏拦截 | 是 | 状态 422、error_class="content_filter",guardrail_blocked 为 true。 |
| 上游错误或超时 | 是 | 每次失败的尝试各一条事件,携带该次尝试的 attempt_index、attempt_kind、attempt_model、error_class 和 error_message。如果后续某次尝试成功,则它的成功事件即最终事件;如果所有尝试都失败,则最后一次失败尝试自身的事件即最终事件,不会额外再写一条。 |
| 调用方在响应头写出前断开 | 是 | 状态 499、error_class="client_disconnected"、error_message="client closed the request before the response head was written",Token 数量和成本均为零。 |
| 响应正文在被读取前即被丢弃 | 是 | 状态和类别相同,error_message="client closed the request before the response body was streamed"。由于上游已经应答,事件会标明处理它的目标。 |
| 调用方在流式传输中途断开 | 是 | 状态和类别相同,error_message="client closed the request while the response was streaming",携带断开前已投递的 Token。 |
流的 200 响应头发出后上游失败 | 是 | 同一失败若发生在响应头之前会得到的状态,例如 502 或 504;error_class 和 error_message 取自该失败本身。参见响应头发出后失败的流。 |
| 认证失败 | 否 | 只写指标。被拒绝的凭证也不会产生访问日志,因此没有可供按 request_id 关联的记录。 |
| 请求正文解析错误或超过大小限制 | 否 | 只写访问日志和指标。 |
| 未认证的请求 | 否 | 不记录任何内容,这也正是健康检查与发现类路由保持静默的原因。 |
/livez、/readyz、/v1/models、/v1/videos/{id} 和 /v1/videos/{id}/content、OAuth 受保护资源发现路由,以及 /a2a/{agent}/.well-known/agent-card.json | 否 | 这些路由在任何结果下都不产生用量事件。视频作业的工作量已由提交请求计量。 |
缓存命中是用所存响应作答的,因此它的事件描述的是调用方寻址的条目,而不是任何目标:
provider_model_version标明产出所存响应的那个模型,它读自缓存条目而非本次请求,与当初那次调用自身事件上的值相同。对模型组而言,这是该条记录上唯一能指出产出方的字段。仅当所存响应本身没有携带模型名时它才为空。provider_request_id保持为空。它是客户用来与模型服务提供方控制台对账的标识,而本次请求并未到达任何模型服务提供方。- 在 routing 或 semantic 条目的命中上,由 Provider Key 派生的字段——
provider_kind、provider_featured、branded_provider、pk_label和byo_label——为空,provider上报unknown。1.2.0 及更早版本的网关会用该请求路由策略排在首位的那个目标来填充它们,从而标注了一个从未运行过的目标,而且同一条缓存条目的两次命中还可能标注不同的目标。由此带来一个变化:按 Provider Key 标签筛选请求日志时不再返回模型组的缓存命中,而按模型服务提供方统计的请求面板会把它们计入provider="unknown",旁边的cache_status="hit"说明了原因。 - 直连模型的命中仍保留该模型自身的模型服务提供方、Provider Key 和上游模型,它们是该模型的静态属性,而不代表发生过下发。
该请求的访问日志遵循相同规则,参见解读缓存判定结果。
被取消请求的 requested_model 是调用方寻址的条目——对于路由请求即模型组名称——model_id 则是它已经确定的目标。只有在没有任何尝试落定、且调用方寻址的条目本身就是模型组时,model_id 才为空:模型组本身不产生费用,其标识符永远不会写入该字段。而在任何尝试开始前被取消的直连模型仍会记录自己的标识符,这与 model_not_found 事件采用的约定一致。被取消的请求也不携带安全护栏归因,即使某次尝试已经应答也不携带 Token 数量,因为这两者都产生于网关来不及执行的响应处理阶段。
调用方不指定模型的路由——/mcp、/a2a/{agent}、透传命名空间、升级前的 /v1/realtime,以及文件、批处理和微调相关端点——同样会写入这条 499 记录。它们的模型字段为空,由各自的归因字段代替:passthrough_route_name,mcp_server_name 与 mcp_tool_name,或 a2a_agent_name 与 a2a_method、a2a_operation。该记录仍然携带 api_key_id、调用方身份、auth_type、operation 和 inbound_protocol。
响应头发出后失败的流
流式响应会在上游产出答案之前就发出 200 状态行,因此之后到达的失败无法再改变客户端收到的内容。用量记录在流结束时写入,它报告的是流实际如何结束,而不是客户端响应行上的 200:
| 流的结束方式 | 记录的状态 |
|---|---|
| 上游断开连接,或发送了 AISIX 无法解码的内容 | 502 |
| 流中途读取上游超时 | 504 |
上游在流内发送了携带 4xx 状态的错误事件 | 该状态。Anthropic 的流内错误按 Anthropic 为该错误类型记载的状态映射,因此 rate_limit_error 记录为 429。 |
上游在流内发送了其他错误事件,包括 5xx 状态,以及不携带 HTTP 状态的 Responses API error 或 response.failed 事件 | 502 |
桥接的 /v1/responses 上游流不含任何内容、推理、工具调用或结束原因 | 502,error_class="stream_aborted",消息说明上游返回了空流 |
| 调用方在流结束前离开 | 499,error_class="client_disconnected" |
| 流正常结束 | 200 |
即使客户端收到的是 200,error_class 和 error_message 描述的也是失败本身。上游失败与调用方断开同时发生时,记录的是上游失败:转发传输错误的中继会中止连接,这与调用方离开看起来完全一样。
以上规则适用于 /v1/chat/completions(包括流式 ensemble 的 judge,panel 成员的记录仍为 200)、原生与转换路径上的 /v1/responses 和 /v1/messages、流式 /v1/audio/transcriptions、/v1/audio/speech,以及透传路由。携带可识别信封(OpenAI Chat Completions、Completions 或 Responses,以及 Anthropic Messages)的透传路由会像类型化端点一样读取流内错 误事件;raw 路由没有可识别的错误信封,只记录传输失败和读取超时。
以下结果不变:安全护栏拦截保持其自身状态,已产出内容但未带结束原因就结束的流仍为 200,正常结束的流仍为 200。
由于这些流现在被记录为失败,控制台的成功率会相应下降,它们也会退出只统计 2xx 请求的延迟分位数。aisix_usage_events_emitted_total 上的 status 和 status_code 标签同样随之变化,请求的访问日志行与其用量事件保持一致。成本、预算和限流不依赖状态,因此不受影响。
1.4.0 及更早版本的网关会把大多数此类流记录为 200,error_class 和 error_message 为空。在其转换路径的 /v1/messages 上,上游失败被记录为 499;上游断开的流式转录也是如此。
由于这些取消记录此前并不存在,环境的请求数现在会包含被放弃的请求,升级后成功率可能下降。控制面的这两个数值都由用量事件推导而来。延迟分位数不受影响,因为它们本来就只统计成功的请求。
访问日志、用量事件与 Logs 页面
这三种信号计量的对象不同,因此发生重试的请求产生的数量也不同:
| 信号 | 每个请求的数量 | 在哪里查看 |
|---|---|---|
| 访问日志 | 无论结果如何,恰好一条 | 网关的标准错误流 |
| 用量事件 | 每次尝试一条,外加该请求的最终事件 | 可观测性导出器以及控制面 |
| Logs 页面记录 | 每条用量事件一行 | AISIX Cloud 请求日志 |
请通过 request_id 关联它们,调用方也会以 x-aisix-request-id 收到该值。在同一个请求内部,按 attempt_index 对各次尝试排序,并通过 attempt_kind 区分重试与故障转移。由于三者数量不同,拿记录条数与请求指标作比较就是在比较两种不同的单位——请先阅读区分请求计数与尝试计数。
在高于 1.2.0 的网关上,流式请求的访问日志在流结束时与最终用量事件一同写出,因此两者的 status、error_kind/error_class 和失败消息一致。