BFE AI 访问日志可观测字段升级实战:`bfe-access-pb` 协议对齐与 `ai_*` 新字段采集改造

发布时间:2026/10/10 5:34:36
BFE AI 访问日志可观测字段升级实战:`bfe-access-pb` 协议对齐与 `ai_*` 新字段采集改造
后端网络/通信云原生【免费下载链接】bfeA modern layer 7 load balancer from baidu项目地址https://gitcode.com/gh_mirrors/bf/bfe点击查看免费下载本文以 BFE 开源仓库Go 语言实现的七层负载均衡器中 AI 网关能力为背景完整解读一份关于 AI 访问日志可观测字段升级的设计变更方案。文章从协议字段重命名与新增的动机出发逐层展开bfe-access-pb依赖升级、AiBasicInfo/AiAuthInfo结构扩展、各 AI 模块采集逻辑填充、访问日志赋值改造、测试计划、兼容性与回滚策略并辅以仓库源码作为实现证据。读者读完可掌握在 BFE 中为 AI 请求补齐 provider、retry、cost、路由命中、cluster/key 尝试等观测字段的完整改造路径。1. 背景bfe-access-pb协议升级带来的配套改造需求BFE 通过mod_access_pb3模块输出基于 protobuf 的访问日志b2log。随着 AI 网关业务的发展承载访问日志协议的独立仓库bfe-access-pb完成了 AI 可观测字段的扩展与重命名协议定义详见bfe-access-pb/docs/protobuf.md。本次设计变更的核心目标就是让 BFE 侧的访问日志代码与新版协议对齐从而把 AI 请求在网关内部的关键行为完整、安全地落进日志。1.1 字段重命名编号不变旧字段名新字段名语义变化ai_apikeyai_apikey_id不再记录原始 API Key 值改为记录 API Key 内部标识ai_mapped_modelai_target_model记录路由/映射后的目标模型ai_prompt_tokensai_input_tokens记录输入prompttoken 数三个字段的 protobuf 编号保持不变分别为 701、704、706二进制层面完全兼容变化只体现在生成代码的 Go 字段名上。1.2 新增字段字段含义ai_provider上游模型提供商标识如 openai、deepseekai_retry_count模型调用层重试次数ai_cost_value/ai_cost_currencyRMB 成本计量与币种ai_route_rule_hits命中的 AI 路由规则列表ai_cluster_key_names请求处理过程中尝试过的 (cluster, key) 列表ai_auth_hit_quota_plans正常请求时命中的 Quota Plan ID 列表1.3 新增辅助消息AIRouteRuleHit描述命中路由规则的 owner / owner_type / nameClusterKeyName描述尝试过的 cluster 与 key 名称组合。变更发生前BFE 依赖的是github.com/bfenetworks/bfe-access-pb v0.1.0访问日志代码中引用的仍是旧字段名AiApikey、AiMappedModel、AiPromptTokens因此需要一次系统性的配套改造。2. 目标与变更总览本次改造的六项目标将 BFE 依赖的bfe-access-pb升级到包含新字段的版本方案建议v0.2.0或本地 replace修正访问日志中对重名字段的引用将ai_apikey_id的数据源从原始 API Key 改为 API Key 内部 IDToken.KeyId/AiBasicInfo.ClientKeyId在 BFE 各 AI 模块中补充新增字段的采集逻辑更新单元测试bfe_modules/mod_access_pb3/request_log_test.go保持非 AI 请求和未升级配置场景下的向后兼容。从仓库当前实现看目标 3 已在token_rule_table.go中落地ValidateUserTokenByReq在鉴权早期就把token.KeyId写入aiBasicInfo.ClientKeyId见 token_rule_table.go保证即使请求后续被拒绝访问日志仍能识别 token 身份。下表是方案给出的全量字段映射Proto 字段 → 编号 → 旧 BFE 字段/状态 → 新 BFE 数据源 → 需修改的文件Proto 字段编号旧 BFE 字段/状态新 BFE 数据源需修改的文件ai_apikey_id701AiApikey记录ClientApiKeyAiBasicInfo.ClientKeyIdrequest_log.goai_apikeytags702已支持AiBasicInfo.ApikeyTags不变ai_requested_model703已支持AiBasicInfo.ClientModel不变ai_target_model704AiMappedModelAiBasicInfo.TargetModelrequest_log.goai_stream705已支持req.IsSse不变ai_input_tokens706AiPromptTokensTokenUsage.PromptTokensrequest_log.goai_output_tokens707已支持TokenUsage.CompletionTokens不变ai_total_tokens708已支持TokenUsage.UsedQuota不变ai_ttft_us709已支持TokenTimeInfo.TTFT不变ai_tpot_us710已支持TokenTimeInfo.TPOT不变ai_rate_limit_hits711已支持AiRateLimitHitInfo不变ai_auth_reject_reason712已支持AiAuthInfo.RejectReason不变ai_auth_reject_quota_plans713已支持AiAuthInfo.RejectQuotaPlans不变ai_provider714缺失cluster.AIConf.Providerrequest_ai_basic.go,reverseproxy.goai_retry_count715缺失AiBasicInfo.RetryCountrequest_ai_basic.go,reverseproxy.goai_cost_value761缺失TokenUsage.UsedCostrequest_log.goai_cost_currency762缺失cluster.AIConf.ModelTable.Currencyrequest_ai_basic.go,reverseproxy.go,request_log.goai_route_rule_hits801缺失AiRouteResult→AIRouteRuleHitrequest_ai_route.go,request_log.goai_cluster_key_names802缺失AiBasicInfo.ClusterKeyNamesrequest_ai_basic.go,reverseproxy.go,request_log.goai_auth_hit_quota_plans841缺失AiAuthInfo.HitQuotaPlansrequest_ai_basic.go,token_rule_table.go,request_log.go3. 依赖升级bfe-access-pb版本替换涉及文件go.mod方案要求将bfe-access-pb从v0.1.0升级到包含新字段的版本require ( ... github.com/bfenetworks/bfe-access-pb v0.2.0 ... )本地开发阶段可临时启用 replace 指向本地源码目录replace github.com/bfenetworks/bfe-access-pb ../bfe-access-pb升级后执行cd bfe go mod tidy go mod download注意bfe-access-pb的.pb.go文件需要在 Linux 环境下执行build.sh重新生成并打 tag 后BFE 才能引用到正确版本。从当前仓库的 go.mod 可以看到依赖已演进到github.com/bfenetworks/bfe-access-pb v0.3.6且go.sum随go mod tidy自动更新说明该升级链路已在仓库中完成闭环。4. 核心数据结构扩展AiBasicInfo与AiAuthInfo4.1 扩展AiBasicInfo涉及文件request_ai_basic.go方案在AiBasicInfo中新增四个字段与一个辅助结构Provider上游 provider如 openai / deepseek、RetryCount模型调用层重试次数、CostCurrency成本币种如 RMB / USD、ClusterKeyNames尝试过的 (cluster, key) 列表并定义ClusterKeyName描述一次尝试的 cluster 与 key 名称组合type AiBasicInfo struct { ClientApiKey string ClientKeyId string ClientModel string TargetModel string Provider string // 新增上游 provider如 openai / deepseek RetryCount uint32 // 新增模型调用层重试次数 CostCurrency string // 新增成本币种如 RMB / USD tokenUsage TokenUsage ApikeyTags []ApikeyTag TokenTimeInfo TokenTimeInfo AiAuthInfo AiAuthInfo ClusterKeyNames []ClusterKeyName // 新增尝试过的 (cluster, key) 列表 allowEstimateToken bool } // ClusterKeyName 描述一次尝试的 cluster 与 key 名称组合 type ClusterKeyName struct { ClusterName string KeyName string }同时新增两个辅助方法func (aiinfo *AiBasicInfo) AppendClusterKeyName(clusterName, keyName string) { aiinfo.ClusterKeyNames append(aiinfo.ClusterKeyNames, ClusterKeyName{ ClusterName: clusterName, KeyName: keyName, }) } func (aiinfo *AiBasicInfo) IncrementRetryCount() { aiinfo.RetryCount }从当前仓库源码看这两处扩展均已落地request_ai_basic.go 中AiBasicInfo已包含Provider、RetryCount、CostCurrency、ClusterKeyNames字段AppendClusterKeyName与IncrementRetryCount方法也存在于 request_ai_basic.go。4.2 扩展AiAuthInfo涉及文件request_ai_basic.go在AiAuthInfo中新增HitQuotaPlans用于记录成功鉴权时参与余额检查并放行的 Quota Plan IDtype AiAuthInfo struct { RejectReason string // 拒绝原因 RejectQuotaPlans []string // 拒绝时余额不足的 Quota Plan IDs HitQuotaPlans []string // 新增成功时命中的 Quota Plan IDs }该结构在仓库中的实现与方案完全一致见 request_ai_basic.go。5. 模块数据填充把采集逻辑写入各 AI 模块5.1mod_ai_token_auth记录命中配额计划涉及文件token_rule_table.go在ValidateUserTokenByReq的for _, plan : range token.QuotaPlans循环中当 Quota Plan 通过余额检查hasBalance true时将其 ID 记录到AiAuthInfo.HitQuotaPlans// 在 for _, plan : range token.QuotaPlans 循环内 if !hasBalance { SetAiAuthInfo(req, bfe_basic.CodeQuotaExhausted, []string{plan.Id}) ... } // 新增记录成功命中的 quota plan aiBasicInfo : req.GetAiBasicInfo() if aiBasicInfo ! nil { aiBasicInfo.AiAuthInfo.HitQuotaPlans append(aiBasicInfo.AiAuthInfo.HitQuotaPlans, plan.Id) }说明Unlimited或PassNoQuota的 plan 不经过HasBalance检查因此不会进入HitQuotaPlans。这符合语义HitQuotaPlans仅记录实际参与余额校验并命中的计划。仓库中的实现印证了这一点token_rule_table.go 中先跳过plan.Unlimited || plan.PassNoQuota再依次处理过期、余额不足写入RejectQuotaPlans与通过追加到HitQuotaPlans三种情况。5.2bfe_server/reverseproxy.go记录 provider、currency、retry、cluster/key涉及文件reverseproxy.goA. 在doSingleAIForward中记录 provider、currency 和 cluster/keyfunc (p *ReverseProxy) doSingleAIForward(..., selectedKey cluster_conf.AIKey) (...) { ... if cluster.AIConf ! nil aiMeta ! nil { if cluster.AIConf.Provider ! { aiMeta.Provider cluster.AIConf.Provider } if cluster.AIConf.ModelTable ! nil cluster.AIConf.ModelTable.Currency ! { aiMeta.CostCurrency cluster.AIConf.ModelTable.Currency } aiMeta.AppendClusterKeyName(cluster.Name, selectedKey.Name) } ... }注意需要确认cluster对象是否有Name字段如果没有使用attempt.ClusterName。仓库中的doSingleAIForward实现在 reverseproxy.go 中Provider/CostCurrency赋值与aiMeta.AppendClusterKeyName(cluster.Name, selectedKey.Name)调用均与方案一致且确认cluster对象具备Name字段。B. 在aiClusterInvoke中统计重试次数for retry : 0; retry policy.MaxRetries; retry { if retry 0 { if aiMeta ! nil { aiMeta.IncrementRetryCount() } ... } ... }仓库实现中aiClusterInvoke的 key-level 重试循环for retry : 0; retry policy.MaxRetries; retry在每次重试前调用aiMeta.IncrementRetryCount()见 reverseproxy.go。说明RetryCount只统计同一 cluster 内 key-level 的重试次数与 HTTP 层basicReq.RetryTime解耦。fallback 到另一个 cluster 时该计数器不累加符合协议语义。这与访问日志中已有的backend_retry字段记录 HTTP 层重试见 request_log.go形成互补一个是模型调用层视角一个是 HTTP 转发层视角。5.3 数据源的配置层面支撑cluster.AIConf从配置加载代码可以看到AIConf位于 cluster_conf_load.go包含Provider模型价格表中的 provider 名称与ModelTable含Currency字段等配置。配置测试用例覆盖了RMB与USD两种币种见 cluster_conf_load_test.go。这意味着ai_provider与ai_cost_currency的实际取值直接来自集群的 AI 配置项而非运行时推导。6. 访问日志赋值改造reqAiInfoGen涉及文件request_log.go方案要求将reqAiInfoGen函数更新为使用新字段名并填充新增字段。核心逻辑如下func reqAiInfoGen(reqLog *bfe_access_pb3.RequestLog, req *bfe_basic.Request, res *bfe_http.Response) { aiInfo : req.GetAiBasicInfo() if aiInfo nil { return } // API Key ID不再记录原始 key if aiInfo.ClientKeyId ! { reqLog.AiApikeyId proto.String(aiInfo.ClientKeyId) } // API Key Tags if len(aiInfo.ApikeyTags) 0 { for _, tag : range aiInfo.ApikeyTags { reqLog.AiApikeytags append(reqLog.AiApikeytags, bfe_access_pb3.ApikeyTag{ Tagname: proto.String(tag.TagName), Tagvalue: proto.String(tag.TagValue), }) } } // Model if aiInfo.ClientModel ! { reqLog.AiRequestedModel proto.String(aiInfo.ClientModel) } if aiInfo.TargetModel ! { reqLog.AiTargetModel proto.String(aiInfo.TargetModel) } // Provider if aiInfo.Provider ! { reqLog.AiProvider proto.String(aiInfo.Provider) } // Stream reqLog.AiStream proto.Bool(isStreamResponse(req, res)) // Token usage usage : aiInfo.GetTokenUsage() if usage ! nil { reqLog.AiInputTokens proto.Int64(usage.PromptTokens) reqLog.AiOutputTokens proto.Int64(usage.CompletionTokens) reqLog.AiTotalTokens proto.Int64(usage.UsedQuota) if usage.UsedCost 0 { reqLog.AiCostValue proto.Int64(usage.UsedCost) } } // Cost currency if aiInfo.CostCurrency ! { reqLog.AiCostCurrency proto.String(aiInfo.CostCurrency) } // Retry count if aiInfo.RetryCount 0 { reqLog.AiRetryCount proto.Uint32(aiInfo.RetryCount) } // TTFT / TPOT ti : aiInfo.TokenTimeInfo if ti.TTFT 0 { reqLog.AiTtftUs proto.Int64(ti.TTFT) } if ti.TPOT 0 { reqLog.AiTpotUs proto.Int64(ti.TPOT) } // Auth reject info if len(aiInfo.AiAuthInfo.RejectReason) 0 { reqLog.AiAuthRejectReason proto.String(aiInfo.AiAuthInfo.RejectReason) } for _, item : range aiInfo.AiAuthInfo.RejectQuotaPlans { reqLog.AiAuthRejectQuotaPlans append(reqLog.AiAuthRejectQuotaPlans, item) } for _, item : range aiInfo.AiAuthInfo.HitQuotaPlans { reqLog.AiAuthHitQuotaPlans append(reqLog.AiAuthHitQuotaPlans, item) } // Route rule hits if routeResult : req.GetAiRouteResult(); routeResult ! nil { reqLog.AiRouteRuleHits append(reqLog.AiRouteRuleHits, bfe_access_pb3.AIRouteRuleHit{ RuleOwner: proto.String(routeResult.Owner), RuleOwnerType: proto.String(routeResult.RouteType), RuleName: proto.String(routeResult.RuleName), }) } // Cluster / key attempts for _, ckn : range aiInfo.ClusterKeyNames { reqLog.AiClusterKeyNames append(reqLog.AiClusterKeyNames, bfe_access_pb3.ClusterKeyName{ ClusterName: proto.String(ckn.ClusterName), KeyName: proto.String(ckn.KeyName), }) } // Rate limit hit info保持不变 hitInfo : req.GetAiRateLimitHitInfo() if hitInfo ! nil len(hitInfo.HitPolicyDict) 0 { for policyId, info : range hitInfo.HitPolicyDict { ... } } }仓库中的reqAiInfoGen实现见 request_log.go与方案完全对齐并在此基础上做了进一步细化AiApikeytags多填充了Taglevel字段Token 部分补充了CacheReadTokens、CacheWriteTokens、AudioInputTokens等细分 token 字段Rate Limit 部分区分了 tpm/rpm/concurrency/redis_error 四种命中类型。6.1AiRouteResult结构路由命中信息来自请求上下文中的AiRouteResult定义于 request_ai_route.go包含RouteTypeapikey / entity / global、Owner路由表 owner、RuleName命中规则名并通过SetAiRouteResult/GetAiRouteResult在请求上下文中传递。方案中可选为AiRouteResult增加导出方法便于request_log.go读取从当前实现看字段本身已可直接访问。6.2 敏感信息防护原始 API Key 永不落日志值得注意的是仓库中requestLogGen在调用reqAiInfoGen之后还有一道maskSensitiveCredentials(requestLog, req)兜底见 request_log.go作为日志输出前的最后防线确保原始 API Key 不会进入任何字段。这与本次改造把ai_apikey_id数据源改为内部key_id的语义方向一致二者共同落实了日志中不泄露敏感凭证的硬性要求。7. 涉及文件清单文件修改内容bfe/go.mod升级bfe-access-pb到v0.2.0或启用 replacebfe/go.sum随go mod tidy自动更新bfe/bfe_basic/request_ai_basic.goAiBasicInfo新增Provider、RetryCount、CostCurrency、ClusterKeyNames新增ClusterKeyName结构及辅助方法AiAuthInfo新增HitQuotaPlansbfe/bfe_basic/request_ai_route.go可选为AiRouteResult增加导出方法便于request_log.go读取bfe/bfe_modules/mod_ai_token_auth/token_rule_table.goValidateUserTokenByReq中记录成功命中的HitQuotaPlansbfe/bfe_server/reverseproxy.godoSingleAIForward记录 provider / currency / cluster-keyaiClusterInvoke统计 retry countbfe/bfe_modules/mod_access_pb3/request_log.goreqAiInfoGen使用新字段名并填充新增字段bfe/bfe_modules/mod_access_pb3/request_log_test.go更新测试断言覆盖新字段8. 测试计划8.1 单元测试涉及文件request_log_test.go更新TestReqAiInfoGen覆盖以下断言变更将ClientApiKey替换为ClientKeyId并断言AiApikeyId将AiMappedModel断言改为AiTargetModel将AiPromptTokens断言改为AiInputTokens新增断言AiProvider、AiRetryCount、AiCostValue/AiCostCurrency、AiRouteRuleHits、AiClusterKeyNames、AiAuthHitQuotaPlans。测试示例aiInfo : bfe_basic.AiBasicInfo{ ClientKeyId: key-id-123, ClientModel: model-a, TargetModel: model-b, Provider: deepseek, RetryCount: 1, CostCurrency: RMB, ClusterKeyNames: []bfe_basic.ClusterKeyName{ {ClusterName: cluster-a, KeyName: key-001}, }, ... } usage.UsedCost 5000 // 1e-8 元从仓库测试代码看上述断言均已实现TestReqAiInfoGen构造了ClientKeyId: key-id-123的AiBasicInfo并分别断言AiProvider deepseek、AiCostValue 5000、AiRetryCount 1、AiAuthHitQuotaPlans长度为 2、AiRouteRuleHits长度为 1、AiClusterKeyNames长度为 1见 request_log_test.go另有测试验证路由命中 owner 与 quota plan ID 的精确匹配以及未鉴权场景下AiAuthHitQuotaPlans应被清空见 request_log_test.go。8.2 编译验证cd bfe go build ./... go test ./bfe_modules/mod_access_pb3/...8.3 集成验证启用 AI 网关发起一次带 API Key 的模型请求收集mod_access_pb3输出的 b2log解码RequestLog校验字段ai_apikey_id等于 Token 的key_id而不是原始keyai_target_model正确反映路由/映射后的模型ai_provider、ai_retry_count、ai_cost_value、ai_cost_currency非空RMB 配额场景ai_route_rule_hits、ai_cluster_key_names、ai_auth_hit_quota_plans与请求行为一致。9. 兼容性说明字段重命名proto 字段编号不变701、704、706因此 protobuf 二进制层面完全兼容变化只体现在生成代码的 Go 字段名上。语义变化ai_apikey_id从记录原始 API Key 改为记录内部key_id避免在日志中泄露敏感信息。需要确认上游日志消费方不再依赖原始 key 值。新增字段均为optional对未升级的旧 BFE 版本无影响。版本依赖升级bfe-access-pb后旧 BFE 代码无法直接编译因此这是一个需要同步发布的破坏性变更仅对 BFE 代码编译层面。从仓库实现看非 AI 请求向后兼容也已落地reqAiInfoGen开头对aiInfo nil直接返回request_log.go非 AI 请求不会进入 AI 字段填充逻辑。10. 风险与回滚10.1 主要风险风险说明规避措施编译失败新 proto 字段名与旧 BFE 代码不匹配按本方案一次性更新所有引用日志消费方依赖旧字段名下游解析ai_apikey、ai_mapped_model、ai_prompt_tokens会失败提前通知下游按 proto 编号而非字段名解析或在下游做映射ai_apikey_id为空如果Token.KeyId未配置日志中将缺失 key 标识确保ai-gateway-api导出的 Token 配置始终包含key_id重试计数语义不清RetryCount仅统计 key-level 重试不统计 cluster fallback文档中明确语义访问日志中已有backend_retry字段记录 HTTP 层重试10.2 回滚方案如需回滚到旧协议将bfe/go.mod中的bfe-access-pb版本改回v0.1.0回滚request_log.go到旧字段名AiApikey、AiMappedModel、AiPromptTokens移除request_ai_basic.go、reverseproxy.go、token_rule_table.go中新增字段的采集逻辑重新编译部署。注意回滚后新字段provider、retry、cost 等将不再输出到日志。11. 后续可选扩展ai_route_rule_hits支持多条命中记录当前AiRouteResult只记录最终命中的规则。未来如果路由模块支持记录所有匹配规则可将HitPolicyDict式的列表写入日志。ai_cluster_key_names区分成功与失败尝试当前记录所有尝试可扩展为标记最终成功的 key。ai_auth_hit_quota_plans与 RMB 扣减计划对齐当前记录所有通过余额检查的 plan可与实际扣减计划做交叉验证。12. 小结本次设计变更以bfe-access-pb协议扩展为牵引在 BFE 侧完成了一次从数据结构 → 采集逻辑 → 日志赋值 → 测试覆盖的闭环改造AiBasicInfo/AiAuthInfo承载新增观测数据mod_ai_token_auth、bfe_server/reverseproxy.go负责在请求处理链路中填充mod_access_pb3/request_log.go的reqAiInfoGen负责最终落盘并以字段编号不变保证二进制兼容、以key_id替代原始 key 落实安全要求。对于需要在 BFE 中扩展 AI 可观测性的开发者本文给出的字段映射表、代码插桩点与测试用例可作为直接参考的改造模板。文档生成日期2026-08-19赞分享后端网络/通信云原生【免费下载链接】bfeA modern layer 7 load balancer from baidu项目地址https://gitcode.com/gh_mirrors/bf/bfe点击查看免费下载相关推荐BFE AI 网关访问日志可观测字段设计从认证、路由到计费限流的全链路埋点解析BFE AI 网关访问日志可观测字段设计从认证、路由到计费限流的全链路埋点解析 BFE 在 AI 网关场景下需要把一次 AI 请求在认证、路由、转发、计费、后端网络/通信云原生TypeGraphQL字段级访问日志记录数据访问详情TypeGraphQL字段级访问日志记录数据访问详情 TypeGraphQL允许开发者通过装饰器和中间件实现灵活的字段级访问日志功能帮助追踪数据访问行为、排后端GraphQLAPI设计BFE mod_auth_basic 规则配置详解auth_basic_rule.data 从字段到源码实现BFE mod_auth_basic 规则配置详解auth_basic_rule.data 从字段到源码实现 auth_basic_rule.data 是 B后端网络/通信云原生上一篇CRC32工具箱一站式CRC32校验反转与计算手册下一篇探索OpenSim Core生物力学模拟的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考