04 — 验证与排错¶
本篇收录:(a) end-to-end 验证脚本怎么用;(b) 常见故障的定位 checklist;(c) 上次踩过的回归预警。
测试脚本:scripts/trace-id/test_trace_id_behavior.py¶
目的: 在不改代码的前提下,对 5 种 trace_id 输入路径做端到端验证(发请求 → 等 batch flush → 查 pgsql SpendLogs.session_id)。
用法:
# 默认(连本地 4000 端口)
python scripts/trace-id/test_trace_id_behavior.py
# 指定后端
python scripts/trace-id/test_trace_id_behavior.py --base-url http://localhost:4000
# 调长 batch flush 等待时间
python scripts/trace-id/test_trace_id_behavior.py --wait 20
# 换模型
python scripts/trace-id/test_trace_id_behavior.py --model claude-opus-4-7
前置依赖:
LITELLM_MASTER_KEY在.env里DATABASE_URL或LITELLM_LOCAL_PG_*一组pip install requests psycopg2-binary- 模型存在且支持
mock_response(默认gpt-5.4)
5 个场景:
| 场景 | header / body | 预期 session_id |
|---|---|---|
| A | x-litellm-trace-id: HSH-A-... |
等于 header 值 |
| B | (无) | 自动 UUID |
| C | x-trace-id: HSH-C-... |
等于 header 值(本仓库 fork 新增) |
| D | body litellm_trace_id: HSH-D-... |
等于 body 值 |
| E | x-litellm-session-id: HSH-E-... |
自动 UUID(这个 header 不识别,是负面用例) |
跑通后输出示例:
✅ A_existing_header expected='HSH-A-...' actual='HSH-A-...'
✅ B_no_header expected=None actual='<uuid>'
✅ C_x_trace_id_header expected='HSH-C-...' actual='HSH-C-...'
✅ D_body expected='HSH-D-...' actual='HSH-D-...'
❌ E_x_litellm_session_id expected='HSH-E-...' actual='<uuid>' ← 期望失败
E 场景为什么是 ❌ 但符合预期: x-litellm-session-id 是社区上游某个分支引入的 header,本仓库未识别。脚本里保留它是为了将来上游若加上支持时能立刻发现。
SQL 速查¶
精确查一条:
SELECT request_id, session_id, model, "startTime", spend, status
FROM "LiteLLM_SpendLogs"
WHERE session_id = 'SPAN-2026-04-30-abc123'
ORDER BY "startTime" DESC;
前缀查一组(与 UI Filter 等价):
SELECT request_id, session_id, model, "startTime", spend, status
FROM "LiteLLM_SpendLogs"
WHERE session_id LIKE 'SPAN-2026-04-30-%'
AND "startTime" >= NOW() - INTERVAL '24 hours'
ORDER BY "startTime" DESC
LIMIT 100;
统计某个 trace 下有多少个 span:
SELECT session_id, COUNT(*) AS span_count
FROM "LiteLLM_SpendLogs"
WHERE session_id LIKE 'SPAN-2026-04-30-%'
GROUP BY session_id
HAVING COUNT(*) > 1
ORDER BY span_count DESC;
对照 metadata:
SELECT session_id,
metadata::jsonb -> 'trace_id' AS metadata_trace_id,
metadata::jsonb -> 'session_id' AS metadata_session_id
FROM "LiteLLM_SpendLogs"
WHERE session_id = 'SPAN-2026-04-30-abc123'
LIMIT 5;
参考脚本:
test_trace_id_behavior.py的query_session_id函数。
故障定位 checklist¶
故障 1:access log 有 trace_id,但 SpendLogs.session_id 是 UUID¶
这是历史踩过的真实坑(已修复,但留作回归预警)。
症状:
- 接入方在响应头能拿到
x-litellm-trace-id是自己传的值 - 但 UI / pgsql 里
SpendLogs.session_id总是随机 UUID
根因: 链路 A 与链路 B 是两条独立通道(见 README.md)。早期版本 litellm_pre_call_utils.py 只把 header 写进 metadata_from_headers["trace_id"],没有写 data["litellm_trace_id"],导致 _get_session_id_for_spend_log 取不到值,fallback 到 uuid.uuid4()。
修复点: litellm_pre_call_utils.py:572-590 必须包含:
trace_id_from_header = headers.get("x-litellm-trace-id") or headers.get("x-trace-id")
if trace_id_from_header:
metadata_from_headers["trace_id"] = trace_id_from_header
data["litellm_trace_id"] = trace_id_from_header # ← 这行是关键
回归测试: 跑 test_trace_id_behavior.py 的 A、C 场景,必须 ✅。
故障 2:UI Filter 输入 Session ID 后没有结果¶
先排除显而易见的:
- 时间窗对吗?默认 24h,如果是几天前的请求要拉宽
- 大小写正确吗?
LIKE默认大小写敏感 - 是不是用了顶部那个白色搜索框而不是高级 Filter?见 03-ui-session-search.md
深层排查:
如果 count = 0 但接入方坚称发了 trace_id:
- 检查接入方 header 名拼写(区分
X-Trace-Id和X-Trace-ID、x-trace-id) - 抓 nginx / paas 网关日志,看 header 是否被剥掉了
- 看 LiteLLM 启动日志里
litellm_pre_call_utils.py的 debug 输出(Extracted trace_id from header: ...)
故障 3:响应头 x-trace-id 被覆盖¶
症状: 接入方传 X-Trace-Id: A,但 LiteLLM 响应头返回 x-trace-id: B,且 B 看起来像 UUID。
可能原因:
- 上游 nginx / paas 把请求 header 剥掉了,LiteLLM 收到的是空 header → 自动生成 UUID
- 同一个请求里
x-litellm-trace-id和x-trace-id都传了不同值,x-litellm-trace-id优先生效(见 01-input-channels.md)
验证: 在 LiteLLM 容器里 grep 应用日志,看 access log 中间件那一刻读到的 header 是什么。
故障 4:抽屉打开后只看到一行¶
症状: 点击 Session ID 单元格后抽屉只显示当前这一行,没看到该 session 的其他 request。
原因: 该 session 本身就只有一个 request。多 request 同 session 必须由接入方主动用同一个 trace_id 发多次请求。如果接入方不复用 trace_id(例如 model-adapter 的同一对话每次都生成新 UUID 当 trace),那么自然每行都是独立 session。
验证:
如果 count = 1 那就是预期行为。
关键代码定位(一键跳转)¶
| 关注点 | 文件:行 |
|---|---|
| ContextVar trace_id 定义 | litellm/proxy/request_context.py:4 |
| 链路 A:读 header / 写响应头 | litellm/proxy/middleware/access_log_middleware.py:51-60 |
| 链路 A:写应用 JSON 日志 | litellm/_logging.py:166-168 |
| 链路 B:读 header / 写 data | litellm/proxy/litellm_pre_call_utils.py:572-590 |
| 链路 B:Logging 类接收字段 | litellm/litellm_core_utils/litellm_logging.py:349-351 |
| 链路 B:写 SpendLogs.session_id | litellm/proxy/spend_tracking/spend_tracking_utils.py:499-522 |
| 后端 UI 接口(Session ID 过滤) | litellm/proxy/spend_tracking/spend_management_endpoints.py:1641-1644, 1791-1796, 1893-1907 |
| 前端 Filter 配置 | ui/litellm-dashboard/src/components/view_logs/index.tsx(logFilterOptions) |
| 前端过滤逻辑 | ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx(FILTER_KEYS.SESSION_ID) |
修改 trace_id 相关代码后的本地验证流程¶
- 改完代码,本地
make run启动 LiteLLM - 跑
python scripts/trace-id/test_trace_id_behavior.py --wait 15 - 检查 5 个场景:A/B/C/D 必须 ✅,E 维持 ❌
- 如果改了 UI:手动打开
/ui/logs,Filter Session ID 输入步骤 2 测试出的某个值,确认能查到 - 如果改了响应头逻辑:
curl -i验证响应头里同时回写x-litellm-trace-id和x-trace-id