状态:canonical;更新:2026-10-22。适用于 openless-all/app/linux-egui,以及被两个前端共用的 crates/openless-core。
日志是 Linux 用户唯一能交回来的证据:界面出错时他们既没有控制台,也不会跑 gdb。所以这份文件规定的不是文风,而是**「出问题时能不能从日志里读出结论」**。
| 项 | 值 |
|---|---|
| 文件 | $XDG_DATA_HOME/openless/logs/openless.log(默认 ~/.local/share/OpenLess/logs/) |
| 单文件上限 | 2 MiB |
| 历史份数 | openless.log.1 … openless.log.3(合计 ≤ 8 MiB) |
| 轮转时机 | 进程启动时;只有宿主轮转,UI 窗口进程不轮转(否则宿主手里的句柄会指向被改名的 inode) |
| 终端 | 同时输出到 stderr,级别与文件一致 |
| 导出 | 设置 → 高级 → 调试 → 「导出错误日志」(export_error_logs,原子写) |
| 导出内容 | 当前 openless.log 加全部轮转历史(.3→.2→.1→当前,时间正序),文件头写明版本、系统架构、生效级别与导出时间 |
两个进程(宿主 openless、界面 openless --ui-client)写同一个文件,排查「窗口进程怎么没了」时两端记录在同一处。
| 级别 | 用来记什么 | 判据 |
|---|---|---|
error |
失败。用户可见的能力没做到,或数据可能受损 | 「这条出现时,功能是坏的」 |
warn |
降级但仍然工作:回退到默认设备、重试成功、可选能力缺失 | 「这条出现时,行为与预期不同」 |
info |
状态变化与低频存活证明(热键注册结果、模型加载、云同步相位、会话开始/结束) | 用户报「它不工作了」时,需要看到「上一次好的是哪一步」 |
debug |
周期仪表:心跳、IPC 往返、逐帧/逐事件细节 | 打开后能让 info 的结论落到具体数值 |
trace |
逐条协议/逐样本,只为复现一次具体 bug | 默认关闭,通常只用一次 |
两条硬规则:
- 周期性的东西不许写
info。 每 10 秒一条 = 8640 行/天,轮转后整份日志九成都是它,真正的错误被挤出去。这类日志写debug,info只留状态变化与低频存活证明。- 参考实现:
log_pump_heartbeat(linux-egui/src/main.rs)——状态变化立刻记info,否则每 5 分钟一条,其余debug。
- 参考实现:
error只写失败,不写进度。log::error!("[x] stage=device_id_ok")这类 tracer 会让每次启动看起来都像崩了,必须降到debug。
设置 → 高级 → 调试 → 日志级别(info / debug / trace):
- 选完写进
<data_dir>/logs/level,重启 OpenLess 生效(界面会提示)。 - 这是宿主本地设置,不进 Core 偏好,因此不上云、不影响 Tauri 与移动端。
- 环境变量优先于这里的设置:临时排查不该被上一次的界面选择挡住。
# 全开
OPENLESS_LOG=debug openless
# 只把 Core 的依赖图噪声打开
OPENLESS_LOG=info,openless_core=debug openless
# 环境变量在 systemd 启动的情况下要写进服务定义
systemctl --user edit openless
# [Service]
# Environment=OPENLESS_LOG=debugOPENLESS_LOG 优先,RUST_LOG 作为兼容名也认。语法:
<level>[,<module-prefix>=<level>]...
- 模块前缀按
::边界匹配,最长匹配优先(openless_core::asr=warn会盖住openless_core=debug)。 - 任何无法识别的片段会让整条规则退回默认
info:宁可多记,不可静默少记。 - 默认
info。发布版不需要用户改任何东西就能看到状态变化。
启动时的第一行会写明生效的过滤与是否轮转过:
2026-10-22T23:40:12.423+08:00 [INFO] openless_linux_egui::logging: Linux egui logger ready: level=debug rotated=false path=/home/x/.local/share/OpenLess/logs/openless.log
<本地时区 RFC3339> [<LEVEL>] [<target>] [<thread>] <文件:行> <消息>
target一直在(模块路径),定位「哪一层打的」不用猜。- 线程名与源码位置只在
debug及以上出现,避免info行变长。 - 时间戳是用户本地时区。历史版本写 UTC(结尾
Z),用户对着自己的操作时间找不到对应行。
消息里的固定前缀是约定,用方括号小写,便于 grep '\[fcitx\]':
| 前缀 | 范围 |
|---|---|
[pump] |
宿主循环存活 |
[hotkey] |
热键注册与分发 |
[fcitx] |
fcitx5 addon 通信 |
[ui-host] / [ui-client] |
两个进程各自的 IPC 侧 |
[dictation] |
一次听写的生命周期(Core) |
[insert-diagnostics] |
文本注入 |
[audio] / [audio-cue] / [audio-mute] |
采集与提示音 |
[style] |
风格包 |
[remote-input] |
手机/远程输入 |
[capture] |
截图与选区 |
[dictation] start session_id=… target=Dictation asr=dashscope insert=true translate=false silence_after_ms=None
[audio] capture started backend=pipewire device=… format=F32 rate=48000Hz channels=1
[transcript] partial emitted session=… ← 仅 debug
[asr] session_id=… provider=dashscope label=… elapsed_ms=1840 chars=42
[asr-diagnostics] session_id=… asr_chars=42 asr_hash=… corrected_chars=42 …
[polish-diagnostics] session_id=… stage=decision uses_polisher=true …
[insert-diagnostics] session_id=… attempt=1 operation=… chars=42 hash=…
[dictation] session … failed: code=… message=… ← 失败时(warn)
同一次听写里的多行用 session_id 串起来([dictation]、[insert-diagnostics] 都带)。排查顺序:
grep 'session_id=<id>' ~/.local/share/OpenLess/logs/openless.log*失败原因现在会同时给出 code 与 message(见 mark_dictation_failed)。历史版本只有 code,因此「录音停止 / 转写 / 润色」这一步光看日志分不出来。
- 任何凭据:API key、token、password、私钥、keystore 内容。日志会被用户原样贴到 issue 里。
- 用户听写的正文。需要时可记长度或哈希(
[insert-diagnostics]就是这么做的:chars=/hash=)。 - 完整
data_dir之外的路径通常没必要;data_dir本身要记,因为「日志写在哪」是第一个问题。
scripts/egui-logging-contract.test.mjs 会拒绝形如 api_key= / token= / password= 的日志格式化参数,以及 stage= 级别的 error。
- 新增周期日志 → 必须是
debug,或带节流。 - 新增
info→ 必须能回答「状态变了什么」。 - 新增
error→ 必须对应一次真实的失败分支。 - 跑
node scripts/egui-logging-contract.test.mjs。