跳转至

OTLP Receiver 跨项目契约(TFRM-119 / TFRM-114 vNext)

对象:TFRO(tfrs-operator)/ TFRS(TFRobotServer)/ TFROB(TFRobot 内核)项目工程师 / 部署运维

目的:定义计费遥测链路(TFROB 内核 emit + TFRO Operator/Collector 转发 → TFRSManager OTLP Receiver → 中央 TimescaleDB of-record)的协议、字段责任、鉴权、轮换、幂等不变量, 使各项目可并行实施且不互相误导。

事实源:本 vNext 据 TFRM-124 三方对齐 R3 决策定稿(评论 #13187) 重写。任何与 §X 冲突的实现须回 TFRM-124 走升级路径, 不得在子工单内私自改契约。


修订历史

版本 日期 变更 来源
v1(TFRM-119) 初版 Manager 单方撰写,未经 TFRO/TFRS ack
vNext 2026-05-15 新增 §X 幂等不变量;字段表按 signal 拆表 A/B;鉴权据实(静态 header + Phase A/B gating);env 名统一 OTLP_BILLING_TOKEN;resource 链改 Alt-A2(logs 非 metrics);grace 改派生值;§8 Prometheus 指标实装 TFRM-124 #13187 R3 定稿;TFRM-125 落地
v5 2026-07-01 llm_billing_events 新增 manager_key_id(LLM 归属 correlation id = 主 PG llm_managed_keys.id,nullable,不列入 receiver 必填);网关退回哑计量器,org 归属由下游权威反查 TFRM-228 / TFRM-230(子2·串行卡点)
v6 2026-07-01 gateway 模式 receiver 必填锚 organization_idmanager_key_idfilterValid fail-closed 锚随计费锚迁移);otlp_dropped_total reason organization_idmanager_key_id;ConsumptionScheduler LLM 计费改按 manager_key_id 权威反查归属 + 合并 upsert + 孤儿隔离,organization_id(denorm)不再进网关路径扣费决策 TFRM-228 / TFRM-231(子3)

vNext 三方 + TFROB ack 矩阵(TFRM-124 #13187 §3)

决策项 TFRS TFRO TFRM TFROB
ns 共享 Collector(非 per-pod sidecar) n/a
单一 /v1/logs egress n/a
归属 Collector upsert / Server 零持有 ✅(C 改 upsert) n/a
§X.2 冻结不变量 — 传输纯透传 ✅(mock 回归) n/a n/a
§X.2 — LLM event_id/TimeUnixNano 冻结 ⚠️ scope-out n/a ✅(规范) ⛔ 待 ack(hard gate)
§X.2 — resource 量化冻结 (Alt-A2) n/a ✅(Go 量化) n/a
§X.3 fail-loud(删 receivedAt 兜底) ✅(支持) ✅(受益) ✅(认领·已实装) n/a
ConnM2L ❌ → Alt-A2 ✅(自提) ✅(接受) n/a
SLA = 队列持久+幂等,不锚投递时延(含 6min eviction 边界) n/a ✅(①③书面) ✅(ack·与 β 捆绑) n/a
pod 锚点 = semconv k8s.pod.uid ✅(钉死) n/a
token: restart-on-sync + T_propagate≤8min;grace=派生值 n/a ✅(grace 公式) n/a
fan-out sink + per-cluster ramp + 对账 SQL n/a n/a(零影响) ✅(认领·已实装) n/a

唯一残留依赖(Epic 验收 hard gate):TFROB 非本三方成员。 TFROB-481 owner 必须在 TFRM-124 显式 ack vNext §X.2(LLM 行:内核 emit 一次性冻结 event_id+TimeUnixNano、重试复用、 meter.py 命名/单位与 logs attribute 1:1)。未 ack 前 Epic 端到端验收不可达。 升级路径:贴 TFROB-481 链接 + @owner 到 TFRM-124,限期 ack 或留反驳。


0. 授权范围与适用边界

  • Admin API(token rotate/list/revoke):仅限 Manager 平台级管理员(PermissionAdminManage), 没有 per-cluster 细粒度 ACL —— 拥有该权限的管理员可管理任意集群的 OTLP token。 当前业务规模下这是合理简化;如未来组织级 admin 分权出现,再扩展。
  • OTLP Receiver 端点:Collector 持 Bearer token 直接调用,由 token 关联 cluster_id 强制隔离。
  • 本文档不约束 Manager 内部 admin-service 的其他能力,仅约束 OTLP Receiver 暴露面。

1. 端点

环境 URL 后端
生产 https://metrics.turingfocus.cn/v1/logs Manager 127.0.0.1:4318
Staging https://metrics-staging.turingfocus.cn/v1/logs Manager 127.0.0.1:4318
Beta https://metrics-beta.turingfocus.cn/v1/logs Manager 127.0.0.1:4318

域名说明metrics{,-staging,-beta}.turingfocus.cn 沿用 VictoriaMetrics 时代已申请并部署好的证书与域名。TFRM-118 拆除 VM 后,该域名/Nginx conf 复用给 OTLP Receiver(不再新增 otlp.* 域名),权威清单见 docs/deploy/02-network.md §域名规划

  • 协议:OTLP/HTTP,protobuf (Content-Type: application/x-protobuf)
  • 仅接受 POST;Body 上限 8 MiB(超过返回 413)
  • 单一入站 signal = /v1/logs(OTel Logs/Events)。/v1/metrics/v1/traces 显式返回 403 (code 40301)

信号模型(R3 定稿,修订 smell A):两类计费事件都以 OTel Logs signal 经 /v1/logs 上行: - LLM 计费event_type=llm_billing):TFROB 内核 logger.emit() 直接产 OTLP logs。 - 资源利用率event_type=resource_utilization):由 TFRO Operator Alt-A2 发射器 周期读 kubelet /stats/summary、Go 量化冻结后直接 emit OTLP logs

Collector kubeletstats receiver 退为仅 Prometheus 观测,不再参与计费。 R1 设想的「Collector 内 metrics→logs connector(ConnM2L)」按字面不可实现 (stock otel/opentelemetry-collector-contrib 无 metrics→logs connector,OTTL 不跨信号), 已由 Alt-A2 取代(Operator Go 量化比 OTTL 更硬,见 §X.2 责任界)。 Manager 始终单一入站 /v1/logs,零改动。


2. 鉴权

每个集群在 Manager admin-service 配发 一对 Bearer token(PRIMARY / SECONDARY), 通过 Infisical → ESO 同步给集群内 Collector。

Authorization: Bearer <raw-token>
  • 鉴权据实(修订 smell 4):Collector otlphttp/billing exporter 上是静态 header authorization: "Bearer ${env:OTLP_BILLING_TOKEN}" headers_setter auth pipeline。
  • 链路 gating(修订 smell A):整条上行链 gated 于 TFRCluster.Spec.Billing.OTLPEndpoint 非空 Billing.Infisical != nil;代码层默认 Phase A = 不上行(debug-only)。
  • Token 颁发:admin POST /api/v1/admin/clusters/:cluster_id/otlp-tokens/rotate —— 响应只返回一次明文,丢失需重新 rotate。
  • Token 在 DB 只存 sha256 哈希;明文存活在 Infisical:
    /otlp/{cluster_id}/PRIMARY_TOKEN     ← 活跃 token,Collector 取用
    /otlp/{cluster_id}/SECONDARY_TOKEN   ← 上一轮 token,grace 过渡期保留
    
  • 失败行为:Nginx 层缺 Authorization → 401;Manager 层错/撤销/过期 token → 401。

3. 强制 cluster_id 重写(重要)

Manager 始终用 token 关联的 cluster_id 覆盖 OTLP payload 中的 cluster_idResourceLogs.Resource.attributesLogRecord.attributes 两处都重写)。

含义:Collector 即使漏写/写错 cluster_id,落库值仍以 token 为准。其余 attribute 全部原样透传,Manager 不做任何重写。

字段契约 — 表 A:LLM-logs(signal=logs · event_type=llm_billing

字段 产出方 注入方 决策
event_id(UUIDv4·冻结) TFROB 内核 内核 emit ⚠️ blocked-by TFROB-481 §X.2
occurred_at=TimeUnixNano(冻结) TFROB 内核 内核 emit ⚠️ 同上(β 命门)
model_name/provider/prompt/completion/cached_input_tokens TFROB 内核 内核 emit ⚠️ TFROB-481 确认 meter.py 命名/单位 1:1
tenant_name/namespace/organization_id/robot_id/cr_name Collector processor upsert ✅ TFRO C 改 upsert
manager_key_id(归属 correlation id·nullable) LLM Key 网关(vkey metadata) 网关 billing.py emit ✅ TFRM-230(= 主 PG llm_managed_keys.id;下游反查权威 org;per_cluster 恒 NULL)
k8s.pod.uid(锚点) K8s downward API Operator→OTEL_RESOURCE_ATTRIBUTES ✅ 字面 = OTel semconv k8s.pod.uid(杜绝漂移)
cluster_id Manager 鉴权层强制重写

字段契约 — 表 B:resource-logs(signal=logs · event_type=resource_utilization · 经 Alt-A2 Operator 发射器

字段 产出方 注入方 决策
event_id(量化哈希·冻结) TFRO Operator (Alt-A2) Operator emit→LogRecord ✅ Go 量化冻结(比 OTTL 更硬)
occurred_at(量化 bucket·冻结) TFRO Operator Operator emit ✅ 同 event_id 量化值
resource_type/utilization/above_threshold TFRO Operator(读 kubelet/summary) Operator emit
pod_name/container_name TFRO Operator Operator emit
tenant_name/namespace/organization_id/robot_id/cr_name Collector processor upsert
cluster_id Manager 鉴权层强制重写

列名注:hypertable 列 tenant_name 由 TFRM-121 从 tenant_id 重命名而来 —— 实际值是 K8s namespace name 字符串。organization_id / robot_id / cr_name 全 nullable (空字符串 → sink 层 nilIfEmpty 落 NULL),兼容 personal 池 / 渐进接入。

manager_key_id(TFRM-230,仅 llm_billing_events:LLM Key 网关计费归属 correlation id, 值 = Manager 主 PG llm_managed_keys.id(十进制字符串,与 organization_id 同选 TEXT,中央 TS 与主 PG 物理隔离不能用 FK)。网关模式由 vkey user_api_key_metadata.manager_key_id emit(网关本次仅把已有 metadata 字段一并 emit,无需新增 metadata)。gateway 模式 receiver 必填校验锚(TFRM-231 起, 取代原 organization_id fail-closed,见 §8):无 correlation id 即不可归属,缺失=网关配置错,在边界 fail-loud 丢弃。per_cluster 内核/sidecar 旧链路无此字段恒 NULL、不校验(用可信 organization_id)。 反查不到 org(present-but-unresolvable,需查主 PG)不在 receiver 判定,由下游 ConsumptionScheduler (子3)孤儿隔离——与「缺失即丢」是两类失败,分层处理。空属性 → NULL(*string nil),不落空串。 下游据此反查权威 org 归属organization_id(denorm)降级为旁路对账证据(不再进网关路径扣费决策)。 ⚠️ 与 llm_key_events.manager_key_id(= provider_keys.id)是不同实体(术语重载,勿混)。


§X 计费时间戳与幂等键不变量(规范性 · TFRM-124 #13187 R3 定稿)

这是整个 Epic「at-least-once + 幂等」的命门(根因 smell β)。三段责任,任何实现不得违背。

§X.1 幂等键(Manager of-record 口径)

去重键 = (event_id, occurred_at)无时间窗,落库 ON CONFLICT (event_id, occurred_at) DO NOTHING。对投递时延 / 重试 / 多副本免疫 —— 当且仅当 §X.2 成立

§X.2 冻结不变量(产出方硬契约)

  • event_id:逻辑事件创建时点一次性生成(LLM = UUIDv4;resource = 对采样 bucket 量化输入的确定性哈希),冻结;任何重传/重试/多副本复用同一字面值, 禁逐次 now()/wall-clock。
  • occurred_at 载体 = OTLP LogRecord.TimeUnixNano,值 = 业务发生时刻 (LLM 调用结束 / resource 采样 bucket 量化时刻),与 event_id 哈希时间分量同一量化值; 写入后冻结,传输层(SDK BatchProcessor / exporter 重试 / Collector / Operator 发射器) 不得重写
  • 责任界:LLM 链冻结归 TFROB 内核(TFROB-481 硬验收);resource 链归 TFRO (Alt-A2 Operator Go 量化);传输纯透传归 TFRS(SDK 不挂改时戳 processor + mock-collector 回归)。

§X.3 fail-loud(Manager 侧 · TFRM 认领 · 已实装 TFRM-125)

Manager 解析 occurred_at 仅取 TimeUnixNano(首选)/ ObservedTimeUnixNano(次选)。 两者皆空 → 判缺必填字段,进 filterValid 丢弃 (internal/shared/otlp/server/server.go,复用 event_id/tenant_name/namespace 的现有丢弃机制,新增 occurred_at 空值分支);已删除 buildEventdefault: ev.OccurredAt = receivedAt 兜底(兜底接收墙钟使重试得到不同 PK → ON CONFLICT 不触发 → 重复计费)。丢弃同时:(a) 单条 zap.Warnevent_type/event_id; (b) 计数 otlp_dropped_total{reason="occurred_at"}非零即告警(见 §8)。


4. 限流

  • Manager 默认 per-cluster 100 rps,桶容量 200OTLP_RATE_PER_SECOND / OTLP_RATE_BURST 可调)。
  • 超过返回 429 + JSON body {"code":42900,"message":"too many requests"}
  • Collector OTLP exporter 必须启用 retry_on_failure,遇 429 退避重试。

5. 集群内 Collector / Operator 配置(参考 · 全量定稿归 TFRO-44/45)

R3 拓扑:ns 共享 StatefulSet Collector(非 per-pod sidecar;kubeletstats 物理上须 node/ns 级),file_storage 队列 PV-backed(VolumeClaimTemplates per ordinal·RWO)。 完整 Collector logs pipeline + 归属 upsert + k8s.pod.uid downward env 注入 = TFRO-44; Alt-A2 resource 发射器 = TFRO-45。本节仅给 Manager 关心的协议锚点,非 TFRO 实现规范。

exporters:
  otlphttp/billing:
    endpoint: https://metrics.turingfocus.cn       # signal=logs → 实际打 {endpoint}/v1/logs(域名沿用 VM 时代,见 §1)
    headers:
      authorization: "Bearer ${env:OTLP_BILLING_TOKEN}"   # 静态 header(非 headers_setter)
    compression: gzip
    retry_on_failure: { enabled: true, initial_interval: 5s, max_interval: 300s, max_elapsed_time: 6h }
    sending_queue: { enabled: true, storage: file_storage/billing, num_consumers: 4, queue_size: 10000 }

extensions:
  file_storage/billing:
    directory: /var/lib/otelcol/billing-queue       # PV-backed(StatefulSet VolumeClaimTemplates·RWO)

service:
  extensions: [file_storage/billing]
  pipelines:
    logs/billing:
      receivers: [otlp]                              # LLM 内核 + Alt-A2 resource 发射器汇流
      processors: [batch, <归属 upsert processor>]    # tenant_name/namespace/org_id/robot_id/cr_name + k8s.pod.uid
      exporters: [otlphttp/billing]

环境变量 OTLP_BILLING_TOKEN(修订 smell ε,原契约误写 TFRS_OTLP_TOKEN)经 K8s Secret 注入:

env:
  - name: OTLP_BILLING_TOKEN
    valueFrom:
      secretKeyRef: { name: tfrs-otlp-bearer, key: token }

tfrs-otlp-bearerESO(External Secrets Operator)从 Infisical /otlp/{cluster_id}/PRIMARY_TOKEN 同步;Operator watch 该 Secret hash 注解触发 受控滚动重启(restart-on-sync,TFRO-46),令 secretKeyRef env 拿到新 token。

归属注入(修订 smell C)tenant_name/namespace/organization_id/robot_id/ cr_name 由 Collector logs processor 一律 upsert/override(Server 零持有,不以 Server 值为准);pod 锚点字面用 OTel semconv k8s.pod.uid(downward API → OTEL_RESOURCE_ATTRIBUTES),杜绝命名漂移。


6. Token 轮换流程

  1. 运维操作POST /api/v1/admin/clusters/:cluster_id/otlp-tokens/rotate
  2. Manager 内部:写新行(status=active)→ 旧 PRIMARY → Infisical SECONDARY_TOKEN → 新 raw → Infisical PRIMARY_TOKEN → 写 admin audit log → 响应携带 raw(仅此一次)
  3. 集群侧传播(TFRO-46 restart-on-sync):ESO 同步 PRIMARY_TOKENtfrs-otlp-bearer Secret → Operator watch Secret → hash 注解 → Collector 受控滚动重启 拿到新 token。传播上界 T_propagate ≤ 8min(默认)/ ≤4min(收紧),书面保证。
  4. 过渡期:旧 token(SECONDARY)在 grace 窗内仍 active,Collector 仍可用旧 token 推送。
  5. 运维收尾:grace 窗到期后 DELETE 撤销 SECONDARY (DELETE /api/v1/admin/clusters/:cluster_id/otlp-tokens/:token_id)。

grace = 派生值,非魔法常量(修订): OtlpTokenRotationGrace() = max(运营底线 72h, T_propagate_max × 安全系数)internal/admin/service/otlp_token_service.go deriveTokenRotationGrace)。 派生依据:grace 必须 ≥ §6 步骤 3 的 T_propagate 上界,否则旧 token 失效时 Collector 仍持旧 token → 401 风暴;运营另设 72h 底线(值班响应余量)。 默认 8min × 3 = 24min ≪ 72h,故当前 72h 主导;T_propagate 上调时 grace 自动跟随, 杜绝「契约 §6 与代码常量两处手动对齐」漂移。


7. 故障模式与责任

情景 Manager 行为 集群侧期望行为
Manager 不可达 请求失败 file_storage PV 队列持久排队,恢复后吐出
Token 被吊销 401 Collector 持续 401 → 必须 pull 新 PRIMARY(§6 步骤 3)
限流 429 立即返回 退避重试,事件保留队列
Body 超 8 MiB 413 缩小 batch size
occurred_at 缺失 200 ack + 丢弃该条 + otlp_dropped_total 计数告警 排查产出方未冻结 TimeUnixNano(§X.2/§X.3)
protobuf 解析错 400 不应发生;属配置错误

SLA 口径(R3 ack·与 §X.3 捆绑):计费可靠性锚定 「PV 持久队列 + 幂等」不锚定投递时延上界。RWO PVC + StatefulSet VolumeClaimTemplates 语义下每副本独立队列, 「≥2 副本」不构成互为故障转移的 HA;保证走「PV 可重 attach」分支:崩溃副本队列不丢, Pod 按 ordinal 重建并重挂同一 PVC 后 drain。显式边界:节点级(非优雅)故障时 RWO 卷 detach + Pod 重调度默认 ≈6min(kube-controller-manager --pod-eviction-timeout + CSI detach),期间该 ordinal 队列滞留但不丢(已落 PV,重挂后投递)。 在 §X.1「无时间窗、(event_id,occurred_at) 精确去重」框架下,延迟投递 ≠ 丢单, Manager ON CONFLICT 对延迟到达免疫 → ns 共享 Collector 成立。


8. 观测信号(Manager 侧)

Phase A(Zap 日志)

  • otlp_billing_event(每条事件一行,logging/fanout archive 侧)
  • otlp_auth_failed + reason 字段;otlp_rate_limited + cluster_id 字段

Phase B(Prometheus 指标 · 已实装 TFRM-125)

cmd/otlp-receiver 暴露 /metrics(loopback only,与 admin-service 同模式,无额外鉴权, 运维侧经内网/sidecar 抓取)。指标(internal/shared/metrics/otlp.go):

指标 类型 含义
otlp_dropped_total{reason} counter filterValid 丢弃数;reasonevent_id/tenant_name/namespace/occurred_at/manager_key_id
otlp_auth_failed_total{reason} counter Bearer 鉴权失败;reasonmissing_header/malformed_header/token_not_found/token_revoked/token_expired
otlp_events_ingested_total counter 成功落 sink 的事件累计(liveness 探针,非精确计费计数

otlp_dropped_totalreason 取缺失字段裸名event_id/tenant_name/namespace/occurred_at/manager_key_id), 与 server.filterValidmissing 字面量一一对应,不加 missing_ 前缀 —— 告警规则按裸名编写, 杜绝代码/规则两处手动对齐漂移。

reason="manager_key_id"(TFRM-231,取代原 TFRM-213 的 organization_id)仅对 gateway 模式 token 产生: 归属权威收回 Manager 后,gateway 事件必须携带 correlation id manager_key_id(下游反查权威 org), 缺失即不可归属(网关配置错),receiver fail-closed 丢弃。非零即说明 gateway proxy 漏注入计费锚 (整批 fail-closed = 静默掉计费/掉收入),须告警。organization_id 已降级不再 gate;per_cluster 路径无此字段、不产此 reason。

otlp_events_ingested_total 是断流 liveness 探针,不是计费精确计数:primary 失败 + sidecar 重试时同批会被计两次(中央 TS 靠 (event_id,occurred_at) PK 去重,本指标不去重)。 仅用于「rotate 后是否还有事件流入」的存活判断,不得用于对账/计费看板。

otlp_auth_failed_totalreason 而非 cluster_id 打标签:鉴权失败时 token 未解析, cluster_id 不可知(auth 失败 hook 仅有 auth.FailureReason)。这是设计约束,非遗漏。

rotate 健康探针告警规则(scrape 侧 PromQL,rotate 后人工/自动观察):

# 新 PRIMARY 未传播到 Collector(ESO/restart-on-sync 链路故障)
rate(otlp_auth_failed_total{reason=~"token_not_found|token_expired|token_revoked"}[5m]) > 0
# 或:计费链路实际断流(即便 auth_failed 未突增)
increase(otlp_events_ingested_total[15m]) == 0
# 或:产出方未冻结 TimeUnixNano(§X.2 被违反)
increase(otlp_dropped_total{reason="occurred_at"}[10m]) > 0
# 或(TFRM-231):gateway proxy 漏注入 manager_key_id 计费锚 → 整批 fail-closed 静默掉计费
increase(otlp_dropped_total{reason="manager_key_id"}[10m]) > 0

9. Phase 边界 / Sink 选型

Manager 侧 sink 通过环境变量 OTLP_SINK_TYPE 选择:

取值 行为 适用场景
logging(默认) Zap.Info 落地,200 ack 后丢弃数据 本地开发 / 中央 TS 不可用兜底 / Phase A
fanout primary=中央 TS·of-record + archive=Zap 归档双写 Phase B per-cluster ramp 对账期
timescale 仅写中央 TimescaleDB hypertable(TFRM-115) 生产 / UAT(ramp 收敛后)

timescalefanout 的 primary 都是中央 TS,启动阶段 fail-fast 要求 OTLP_TS_HOST/OTLP_TS_PORT/OTLP_TS_USER/OTLP_TS_PASSWORD/OTLP_TS_DBNAME/OTLP_TS_SSLMODE 完整;OTLP_TS_AUTO_MIGRATE=true(默认)启动跑嵌入式 schema migration(DDL 幂等)。

SSL 边界(TFRM-120 收敛):生产形态为 docker on management VM —— 中央 TS 与 otlp-receiver / user-service 同 VM、同 docker network 或 loopback。此场景 OTLP_TS_SSLMODE=disable 是可接受默认。仅当部署形态变为跨 host / 跨 VPC 时切 verify-full 并注入 CA / 客户端证书。

Trust boundary 假设(disable 成立前提,运维与安全责任人共同确认): 1. 中央 TS 容器端口必须 loopback 绑定127.0.0.1:5433:5432);改 0.0.0.0 即等价公网 plaintext。 2. 管理平面 VM 不与不可信容器共享 docker network。 3. 拥有该 VM shell 访问权 = 拥有 TS 明文访问权(与现有 tfrs_postgres 同信任模型)。 三条任一不成立,本节 disable 默认失效,必须回退 verify-full

⚠️ cluster_id 契约:hypertable cluster_id 列为 TEXT(与 Manager PG 物理隔离避免 FK), Manager 鉴权层强制写 strconv.FormatUint(uint64(authClusterID), 10)(十进制字符串)。 下游 Worker(TFRM-117)反向 join 必须用 strconv.ParseUint(clusterID, 10, 64), 不存 hex / UUID / 任何非十进制格式。

Phase B per-cluster ramp checklist(切流单元 = per-cluster)

切流单元 = per-cluster(与 TFRO TFRCluster.Spec.Billing.OTLPEndpoint per-cluster 对齐)。 逐集群推进,不全局一刀切:

  1. 目标集群 Manager 实例 OTLP_SINK_TYPE=logging → fanout,开对账窗口(建议 ≥1 个计费周期)。
  2. fanout 期 primary(TS·of-record)与 archive(Zap 归档)双写;primary 失败原样冒泡触发 Collector 重试(TS 经 §X.1 ON CONFLICT 幂等,重试安全),primary 失败时不写 archive
  3. 跑对账 SQL(下):比该 cluster_id 在对账窗内 TS 侧去重计数 vs Zap 归档侧去重计数。
  4. 差额 ≤ 阈值(建议 0;允许 archive ≥ TS —— archive 在 primary 成功后才写,不会反超)→ 该集群 OTLP_SINK_TYPE=fanout → timescale
  5. 推进下一集群,重复 1–4。

对账 SQL(本工单首次起草;#13187 §2 是决策表非 SQL,无既有 SQL 可引用)

-- TS 侧:某 cluster 对账窗内去重事件数(llm / resource 各跑一次)
SELECT count(*) FROM (
  SELECT DISTINCT event_id, occurred_at
  FROM llm_billing_events            -- 或 resource_utilization_events
  WHERE cluster_id = :cluster_id
    AND occurred_at >= :window_start
    AND occurred_at <  :window_end
) t;

归档侧:在同窗对 Zap 归档(otlp_billing_event 行,按 event_type 过滤、按 (event_id, occurred_at) 去重)计数,与上面 TS 计数比对。差额需为 0(或仅 archive ≥ TS 且差值可由「archive 在 primary 成功后写」解释)方可切 timescale

event_type discriminator(TFRM-115 sum-type)

LogRecord attribute event_type 决定落库 hypertable:

取值 落库表 必填(NOT NULL) 可选维度(nullable)
llm_billing(默认) llm_billing_events event_idoccurred_attenant_namenamespacemodel_nameproviderprompt_tokenscompletion_tokens organization_idrobot_idcr_namemanager_key_id(TFRM-230 归属 correlation id)
resource_utilization resource_utilization_events event_idoccurred_attenant_namenamespaceresource_typeutilizationabove_threshold organization_idrobot_idcr_namepod_namecontainer_name

缺省 event_type 视为 llm_billing(兼容历史);新接入方应显式带该 attribute。 occurred_at 为两表必填且受 §X 不变量约束(缺失 fail-loud 丢弃,非兜底)。 resource_utilization 事件由 §1 所述 Alt-A2 Operator 发射器产出(非 Collector metrics→logs connector)。personal 池场景 namespace 不足以区分扣费主体,需依赖 robot_id/cr_name 归一(TFRM-122 ConsumptionScheduler 精确聚合)。