📊 agentprof 可视化指南 · 目录 Wiki 12 / 14
Wiki

OTLP receiver

启用 otlp feature 后,agentprof-storage 内置一个双栈 OpenTelemetry receiver —— gRPC 在 :4317、HTTP/protobuf 在 :4318,原生收 Claude Code / Codex / Copilot CLI / 任何 OTel SDK 的 logs / metrics / traces 三信号,落到 SQLite store。不需要外置 collector —— agent 直接 push 到 agentprof,本地循环到「写文件 → 跑 cli」的路径在生产场景里消失。架构由 ADR-0021 钉死,安全硬化由 ADR-0022 钉死。

Bearer 比较
subtle::ConstantTimeEq
防 timing attack
单包大小
8 / 2 / 8 MiB
logs / metrics / traces 上限
Session 上限
1024
LRU eviction (M2.4)
session.id 长度
≤ 256 字节
防 GB-OOM (mapper.rs:521)
📡 类比 — 像 Prometheus 的 remote_write endpoint
  • producer:agent (Claude Code / Codex / Copilot CLI) 的 OTel SDK,按 OTLP 规范 push 三信号。
  • wire format:OTLP — gRPC (binary protobuf) 或 HTTP/protobuf;agentprof 两个都接。
  • endpoint:agentprof serve --ingest-otlp(M2.2 子命令),监听 4317/4318。
  • sink:SQLite — 通过 IngestPipelineSessionRouter → 同 schema 的 store DB。

和 Prometheus remote_write 不同的关键点:OTLP 是长连接 + 流式(一个 agent session 可能用一条 gRPC stream 跑数十分钟),所以 agentprof 必须维护 per-session buffer 状态 —— 这就是 SessionRouter 的存在意义,也是 ADR-0022 LRU cap 要保护的对象。

双栈协议对比(recon 真实端口 / 路径)

协议 / 入口默认端口 / 路径适用场景
gRPC
serve_grpc(cfg, pipeline)
:4317(OTel 官方默认)
3 个 service:LogsServiceServer / MetricsServiceServer / TraceServiceServer
高吞吐、长连接、流式 — agent SDK 默认首选;防火墙允许 :4317 时用
HTTP/protobuf
serve_http(cfg, pipeline)
:4318(OTel 官方默认)
3 条路由:POST /v1/logs / POST /v1/metrics / POST /v1/traces
穿透只允许 HTTP/HTTPS 的网络环境;和 reverse proxy(nginx / Caddy)友好;OTel SDK 通常 fallback 选这个
TLS + Bearer
tls.rs + auth.rs
两栈都支持:tls_cert + tls_key(必须成对)+ 可选 tls_client_ca(mTLS)+ listen_token(Bearer)跨主机 / 生产暴露;本地回环可省。Bearer 单独可用(HTTP 上等于裸传 token,不推荐生产)

⚠️ Recon 校正:HTTP 栈当前只支持 protobuf body(没 OTLP/HTTP+JSON 实现路径)。两栈共享同一个 OtlpServerConfig 配置 + 同一个 IngestPipeline 后端,所以混合部署(同时开 gRPC + HTTP)共享 store 状态没问题。

👇 三张卡片:① gRPC vs HTTP 怎么选 · ② ADR-0021 整体架构(含 flow diagram)· ③ ADR-0022 4 层防御逐条拆解。

1 gRPC vs HTTP/protobuf 双栈选择 点击展开
🚦 优先选哪个 — gRPC 还是 HTTP?
默认选 gRPC(:4317):序列化效率高、长连接复用、双向流支持,是 OTel 生态的「first-class」transport。选 HTTP(:4318)当:① 网络只允许 HTTP/HTTPS(企业代理、严格防火墙);② 后端要走 reverse proxy 做 TLS 卸载 / load balance;③ OTel SDK 版本太旧没 gRPC 支持。两栈在 agentprof 这边完全等价 —— 同一份 mapper 把 OTLP typed event 转 TypedEvent 进 pipeline。
🔌 真实接口签名
// crates/agentprof-storage/src/otlp/server_grpc.rs:84
pub async fn serve_grpc(
    cfg: OtlpServerConfig,
    pipeline: Arc<IngestPipeline>,
) -> Result<GrpcServerHandle, OtlpServerError>;

// crates/agentprof-storage/src/otlp/server_http.rs:96
pub async fn serve_http(
    cfg: OtlpServerConfig,
    pipeline: Arc<IngestPipeline>,
) -> Result<HttpServerHandle, OtlpServerError>;

两个 fn 都返回 JoinHandle + oneshot::Sender<()> 的 graceful-shutdown 句柄;serve --ingest-otlp 同时 spawn 两者,await 任一异常即整体退出。

🛠️ 为什么不外置 collector?
业界标准做法是「agent → OTel Collector → backend」三段式。agentprof 把后两段合并是为了降低个人 / 小团队的安装摩擦 —— 不需要先装 collector 配 receiver 配 exporter 配 pipeline yaml 才能看 agent 数据。trade-off 是 agentprof 自己要处理一部分 collector 该做的事(per-signal size cap、bearer auth、LRU eviction)—— ADR-0022 就是补这部分的硬化。如果用户已经有 collector,serve --ingest-otlp 也可以当 backend exporter target —— 兼容并存。
2 ADR-0021 整体架构 + 6 层 pipeline 点击展开
🏗️ ADR-0021 整体架构 — 6 层流水线
OTel SDK receiver router buffer flush sink SQLite
  1. receiverserver_grpc / server_http)— 收 OTLP bytes,过 bearer auth / TLS / per-signal size cap,decode 成 protobuf 结构。
  2. mapperotlp/mapper.rs)— OTLP ResourceLogs / ResourceMetrics / ResourceSpans → agentprof 的 TypedEvent;从 resource attrs 抽 session.id(截断到 256 byte,mapper.rs:521)。
  3. routerotlp/router.rs::SessionRouter)— DashMap<SessionId, SessionBuffer>;新 session 创 buffer、已存在的拿现成的;LRU evict 超过 max_open_sessions = 1024
  4. bufferotlp/router.rs::SessionBuffer)— per-session 攒事件;每 buffer 自己有 OOM cap(默认 16 MiB / 100 000 events / 5 min idle);达到任一阈值触发 flush。
  5. flush sinkotlp/sink_storage.rs)— buffer 满 / session 结束 / idle 超时 → 序列化整 session 的 events → 喂 agentprof-core 的 analyzer 算 AnalysisReport
  6. upsertDb 写)— INSERT OR REPLACE INTO sessions ... WHERE id = ?;同 schema 同 path 同 dual-path 兼容性。
🧵 为什么用 DashMap 而不是 Mutex<HashMap>
OTLP receiver 是 high-concurrency 场景 —— 多个 gRPC stream 并发 push,每个 stream 进 router 找自己 session 的 buffer。Mutex<HashMap> 会让全部 stream 串行化,吞吐塌方。DashMap 是 sharded map(默认 64 段),每个 shard 独立 lock,不同 session 完全并行。Tradeoff:iter 不是一致 snapshot(LRU sweep 时要小心),但 agentprof 的 LRU 是 epoch + heap 维护、不依赖 map iter 顺序。
🔁 graceful shutdown 协议
两个 server fn 都返回 (JoinHandle, oneshot::Sender<()>)。cli 收到 SIGINT/SIGTERM → 通过 oneshot::send(()) 触发;server 内部 select shutdown signal vs tonic / axum 的 graceful 关闭,等 in-flight request 处理完才退;同时 SessionRouter flush 所有 buffer 落 SQLite —— 不丢已收的 event。这是 cli 退出码 0(正常)vs 130(SIGINT)的关键。
3 ADR-0022 4 层防御逐条拆解 点击展开
🛡️ 防御 1:常时间 Bearer 比较(subtle::ConstantTimeEq
// crates/agentprof-storage/src/otlp/auth.rs
use subtle::ConstantTimeEq;
const BEARER_PREFIX: &str = "Bearer ";

// 字符比较走 ConstantTimeEq -- 不让 attacker 用 timing 探测 token 前缀

为什么:naive 的 == short-circuit —— attacker 喂「a」「b」「c」...,agentprof 在错的字节就早退;通过测「拒绝多花了几纳秒」能逐字符暴力。subtle::ConstantTimeEq 对每个字节都走完比较再返结果,时间分布不泄露信息。不防什么:长度差异本身可被旁路检测(agentprof 假设攻击者已知 token 长度 —— 通常 32 字节 hex);这不是问题,密码学社区认可的取舍。

🛡️ 防御 2:per-signal 消息大小上限(max_decoding_message_size / DefaultBodyLimit
问题:tonic 和 axum 的缺省解码限制都很宽(4 MB 量级),但 OTLP 是 protobuf —— 一份 4 MB 的恶意 protobuf 解码后可能膨胀几百 MB(嵌套 list、recursive message)。方案:gRPC 侧每个 service 单独 .max_decoding_message_size(cfg.max_*_request_bytes)(在 InterceptedService 之前);HTTP 侧每条路由单独挂 DefaultBodyLimit::max(...) layer。关键细节server_http.rs 注释里专门标了):router-level middleware 不能 drain body,否则 413 保护会先 OOM 再触发 —— 加 logger / decompressor 时要把它移到 per-route layer 里。
🛡️ 防御 3:SessionRouter LRU eviction (cap = 1024)
// crates/agentprof-storage/src/otlp/router.rs:171
SessionBufferCaps {
    max_bytes:         16 * 1024 * 1024,   // 16 MiB per session
    max_events:        100_000,            // per session
    max_idle:          Duration::from_secs(5 * 60),  // 5 min
    max_open_sessions: 1024,               // 全局 router 容量
}

问题:attacker 用大量不同 session.id 发 1 event 然后停 —— 每个 session 都创 buffer,DashMap 无限增长。方案:第 1025 个 session 来时,evict 最久未触达的那个 —— 触发 CloseReason::CapacityEvict,flush 它的 buffer 到 SQLite,腾出位。取舍:被 evict 的 session 如果之后又有 event 来,会被视作新 session 重新建 buffer —— 极端情况会被切成多段 SessionRef,但数据不丢。1024 这个数字是「单机一天健康活跃 session ≤ 几十、留 20x 余地」拍的,可调。

🛡️ 防御 4:session.id 256-byte 上限
// crates/agentprof-storage/src/otlp/mapper.rs:521
// ADR-0022 D-5: cap session_id at 256 bytes BEFORE allocating
//               the SessionId String -- prevents an attacker
//               from forcing GiB-sized id allocations.

问题:OTLP resource attrs 是 string —— attacker 设 session.id 为 1 GB string,agentprof 在 String::from 时就 OOM。方案:从 wire bytes 取 id 前 先看长度,超 256 直接截断 / reject。256 byte 远超合理 session id(UUID 36 字节、SHA256 64 hex 字符)—— 给 prefix path 也留足空间。这个 cap 在 mapper.rs 里、在 SessionRouter::lookup_or_create 之前,确保恶意 id 永远进不了 router map

🤔 还有没漏的攻击面?
ADR-0022 还有 D-1 / D-4 等条目处理别的边缘(如 tonic 的 TLS 配置必须成对、TLS provider 安装时机)。暂未硬化的有:① per-IP rate limit(依赖前置 reverse proxy);② OTLP attribute key 长度(理论上 attacker 可塞 1 GB 的 key name 进 resource attrs)—— audit list 里记着,但低优先级,没真实场景。

下一步

本课讲清了 OTLP receiver 的双栈、ADR-0021 6 层 pipeline、以及 ADR-0022 4 层防御的真实代码位置。下一课「Web dashboard 架构」翻 agentprof serve 的另一面 —— 5 视图 localhost 看板怎么 reuse M2.2 axum 栈、chunk-endpoint pattern 怎么省掉 React/Vue 整个构建链(ADR-0024 D-1..D-7)。

📂 相关源码: agentprof-storage/otlp/server_grpc.rs  serve_grpc

📂 相关源码: agentprof-storage/otlp/server_http.rs  serve_http

📂 相关源码: agentprof-storage/otlp/router.rs  SessionBufferCaps