Skip to content

feat: add dashboard logs tab#371

Draft
SsuJojo wants to merge 20 commits intoicebear0828:masterfrom
SsuJojo:logs-tab
Draft

feat: add dashboard logs tab#371
SsuJojo wants to merge 20 commits intoicebear0828:masterfrom
SsuJojo:logs-tab

Conversation

@SsuJojo
Copy link
Copy Markdown
Collaborator

@SsuJojo SsuJojo commented Apr 14, 2026

Summary

  • Add a Logs tab to inspect ingress/egress requests with filters, pause/enable controls, and details view
  • Fix log route parsing and request-id propagation edge cases
  • Update changelog (EN/中文)

Test plan

  • npm run build

@icebear0828
Copy link
Copy Markdown
Owner

Review

Thanks for the contribution! The Logs tab is a useful feature. Here's feedback to get it merge-ready.


Blocker: Merge conflict

packages/electron/package.json has literal <<<<<<< HEAD / ======= / >>>>>>> markers — needs to be resolved first.

Blocker: No tests

LogStore, redactJson, admin routes, and useLogs all need tests. We follow TDD in this project — untested code can't be merged.

Blocker: Unrelated changes

test:local-chat script and tests/local-chat/ vitest include are unrelated to the logs feature — please split into a separate PR.


Architecture: avoid manual enqueue() in every route

The current approach copies a 17-line logStore.enqueue() block into chat.ts, messages.ts, responses.ts (×2), and 6 similar blocks in proxy-handler.ts. This is fragile and duplicates data the existing middleware already captures (method, path, status, latency, request ID).

Suggested approach:

Ingress — middleware, not per-route

Create a src/middleware/log-capture.ts that runs after await next() and automatically captures the request record. Route handlers only need to set context variables:

// middleware/log-capture.ts
export async function logCapture(c: Context, next: Next) {
  const start = Date.now();
  await next();
  logStore.enqueue({
    id: randomUUID(),
    requestId: c.get("requestId"),
    direction: "ingress",
    ts: new Date().toISOString(),
    method: c.req.method,
    path: c.req.path,
    model: c.get("logModel"),    // set by route handler
    status: c.res.status,
    latencyMs: Date.now() - start,
    stream: c.get("logStream"),  // set by route handler
  });
}

Each route adds one line instead of 17:

c.set("logModel", req.model);
c.set("logStream", !!req.stream);

Egress — extract a helper, don't restructure try/catch

The PR currently wraps the entire success path in proxy-handler.ts inside a new inner try/catch, which changes error semantics (e.g. if handleNonStreaming throws, it gets logged as an egress error). Instead, keep the existing control flow and extract a one-liner helper:

// src/logs/helpers.ts
export function logEgress(opts: { requestId: string; model: string; provider: string; status: number | null; latencyMs: number; stream: boolean; error?: string }) {
  logStore.enqueue({ id: randomUUID(), direction: "egress", ts: new Date().toISOString(), method: "POST", path: "/codex/responses", ...opts });
}

Then each call site is one line:

logEgress({ requestId, model: req.model, provider: "codex", status: rawResponse.status, latencyMs: Date.now() - startMs, stream: req.isStreaming });

No try/catch restructuring needed — just add one line after withRetry() returns and one in the existing catch.


Config integration

Currently LogStore is hardcoded enabled = true / capacity = 2000. This means:

  • Docker users can't configure it via config/default.yaml
  • Container restarts reset the enabled state
  • No way to control memory usage

Add a logs section to the config schema:

logs: z.object({
  enabled: z.boolean().default(false),
  capacity: z.number().default(2000),
  capture_body: z.boolean().default(false),
}).default({})

Default to disabled — let users opt-in.


Privacy: don't store request bodies by default

The PR logs full request bodies (user prompts, conversation history) and all headers into memory. redactJson strips auth headers but not message content. For a proxy handling third-party traffic this is a privacy concern, and for Docker users with limited memory it's a resource concern.

Guard body capture behind capture_body: true in config, default off.


Smaller issues

  • LogDirection type mismatch: shared/hooks/use-logs.ts defines "ingress" | "egress" | "all" but src/logs/store.ts defines "ingress" | "egress". Separate LogFilterDirection (includes "all") from LogDirection (record-level, no "all").
  • Inconsistent requestId sourcing: chat.ts/messages.ts use c.req.header("x-request-id"), responses.ts uses c.get("requestId"). The middleware already stores it in context — use c.get("requestId") everywhere.
  • Logs are oldest-first: records.push() means the UI shows oldest at top. For a live log viewer, newest-first is more useful.
  • No pagination UI: Hook fetches limit: 50, offset: 0 but there's no page control despite showing total count.
  • Polling when tab inactive: setInterval at 1.5s runs regardless of whether the Logs tab is selected.

Existing logging coverage (FYI)

The project already has comprehensive structured logging — you can see the full picture in the codebase:

  • middleware/logger.ts + request-id.ts: method/path/status/latency/rid per request
  • proxy-handler.ts: account, model, payload size, token usage (cached/uncached/reasoning), large payload warnings
  • proxy-error-handler.ts: 429/402/403/401 classification with account context
  • usage-stats.ts: cumulative tokens/requests, persisted to disk
  • src/utils/logger.ts: structured JSON in production, human-readable in dev

The Logs tab should integrate with this existing system rather than building a parallel one. The middleware approach above achieves this.


Summary

The LogStore ring-buffer, redactJson, admin API design, and frontend LogsPage are all solid work. The main changes needed are:

  1. Resolve merge conflicts
  2. Replace per-route manual enqueue() with middleware (ingress) + helper (egress)
  3. Don't restructure proxy-handler.ts try/catch
  4. Add config schema for logs (enabled/capacity/capture_body)
  5. Default to disabled, no body capture
  6. Add tests
  7. Split out unrelated test:local-chat changes

@SsuJojo SsuJojo marked this pull request as draft April 14, 2026 15:45
@SsuJojo SsuJojo requested a review from icebear0828 April 14, 2026 15:45
@SsuJojo SsuJojo self-assigned this Apr 14, 2026
Summarize oversized request payloads, centralize log entry creation, and add tests for the new log shape and store behavior.
@SsuJojo SsuJojo marked this pull request as ready for review April 15, 2026 03:13
@icebear0828
Copy link
Copy Markdown
Owner

Review — post-refactor commit (e66dc08)

Nice progress! Config integration, request summarization, type separation, and tests are all solid improvements. Here's what's left before merging.


Must fix

1. Remove inner try/catch in proxy-handler.ts

The current diff wraps the entire success path (withRetry → streaming/non-streaming → handleNonStreaming) inside a new inner try/catch. This changes error semantics — if handleNonStreaming or streamResponse throws for a non-upstream reason (e.g. a format bug), it gets caught and logged as an egress error, masking the real exception.

Instead, keep the existing control flow untouched and just add enqueueLogEntry at two points:

// After withRetry returns successfully:
const rawResponse = await withRetry(...);
enqueueLogEntry({ requestId, direction: "egress", method: "POST", path: "/codex/responses",
  model: req.model, provider: "codex", status: rawResponse.status,
  latencyMs: Date.now() - startMs, stream: req.isStreaming });

// ... existing streaming/non-streaming logic unchanged ...

And in the existing outer catch (the CodexApiError handler), add one line:

} catch (err) {
  enqueueLogEntry({ requestId, direction: "egress", ..., error: err.message });
  // existing error handling unchanged
}

No try/catch restructuring needed.

2. Unify requestId sourcing

chat.ts and messages.ts use c.req.header("x-request-id") ?? randomUUID(), while responses.ts uses c.get("requestId"). The request-id middleware already stores it in context — use c.get("requestId") everywhere for consistency.


Nice-to-have (can be follow-up)

  • Middleware for ingress: The per-route 14-line blocks work but are boilerplate-heavy. A logCapture middleware would reduce each route to c.set("logModel", model) — can iterate later.
  • Pagination: Prev/Next buttons are disabled stubs — either wire them up or remove for now.
  • Tab-inactive polling: setInterval(1500ms) runs regardless of tab visibility. Consider document.hidden check or requestAnimationFrame gate.
  • More test coverage: redactJson and admin log routes could use dedicated tests.

Fix items 1–2, and this is good to merge. The rest can be follow-up PRs.

SsuJojo added 3 commits April 15, 2026 18:32
Add the logs dashboard, backend log capture, and admin log routes so request flow can be inspected from the UI.
Reset logs pagination on filter changes, clear stale selected entries, validate logs query params, and add regression coverage for the route/store/hook behavior.
Copy link
Copy Markdown
Owner

@icebear0828 icebear0828 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat: add dashboard logs tab

整体不错,架构清晰,测试覆盖也比较全面。以下是需要关注的问题:


🔴 Critical: 分页逻辑反了

src/logs/store.tslist() 方法:

const sliced = results.slice(offset, offset + limit).reverse();

records 按时间正序 push(最旧在前),slice(0, 50).reverse() 返回的是最旧的 50 条倒序显示。用户期望 page 0 看到最新的记录,但实际上最新的在最后一页。

测试只有 2 条记录没暴露这个问题。应该先对 filtered results 做 reverse 再分页,或者用 slice(-offset - limit) 之类的方式从尾部取。


🔴 Critical: 双重 ingress 日志

src/middleware/log-capture.ts每个请求都记录 ingress 日志,但 chat.tsmessages.tsresponses.ts 各自也 enqueue 了带 model/stream 信息的 ingress 日志。每个 API 请求会产生 2 条 ingress 记录,用户会很困惑。

建议二选一:要么只保留 middleware 的通用日志,要么只保留路由级别的详细日志(后者更有价值)。


🟡 Medium: capture_body 是死配置

Config schema 声明了 capture_body,Settings UI 也能切换,但实际代码中从未读取这个值summarizeRequestForLog 始终只生成摘要,无论 capture_body 是 true 还是 false。要么实现它,要么先移除。


🟡 Medium: requestId 获取逻辑大量重复

chat.tsmessages.tsresponses.tsproxy-handler.ts 中都有:

const requestId = c.get("requestId") ?? randomUUID().slice(0, 8);

重复 5+ 次,建议统一放到中间件或工具函数里。


🟢 Low: Pagination 按钮未 i18n

web/src/pages/LogsPage.tsx 中 "Prev" 和 "Next" 是硬编码英文,其他文本都做了 i18n。


🟢 Low: 不相关的改动混入

packages/electron/__tests__/builder-config.test.ts 删除了 bin 目录相关断言,跟 logs 功能无关,建议拆到单独的 commit/PR。


🟢 Low: getRealClientIp try-catch 改动影响范围

包了 try-catch 吞掉异常返回空字符串。对日志场景合理,但它也改变了 auth middleware 的行为(之前会 throw),可能掩盖连接信息缺失的问题。建议至少加个 warn log。


Summary

级别 问题 建议
🔴 Critical 分页取到的是最旧记录而非最新 修复 list() 排序逻辑
🔴 Critical ingress 日志记录两次 去掉 middleware 或路由级别其中之一
🟡 Medium capture_body 声明但未使用 实现或移除
🟡 Medium requestId 获取重复 5+ 处 抽取到公共位置
🟢 Low Prev/Next 未 i18n 加翻译 key
🟢 Low builder-config 测试改动不相关 拆出去
🟢 Low getRealClientIp 静默 catch 考虑加 warn log

@SsuJojo
Copy link
Copy Markdown
Collaborator Author

SsuJojo commented Apr 16, 2026

🔴 Critical: 双重 ingress 日志

src/middleware/log-capture.ts每个请求都记录 ingress 日志,但 chat.tsmessages.tsresponses.ts 各自也 enqueue 了带 model/stream 信息的 ingress 日志。每个 API 请求会产生 2 条 ingress 记录,用户会很困惑。

建议二选一:要么只保留 middleware 的通用日志,要么只保留路由级别的详细日志(后者更有价值)。

这个问题不存在 因为日志需要记录入口和出口的两次不一样的请求 所以一次对网关的请求会记录两条日志 这很正常

SsuJojo added 3 commits April 16, 2026 15:55
Make the logs store paginate from newest-first results and wire capture_body into request summaries so the dashboard setting actually affects recorded payloads.
Persist a logs.llm_only setting, filter captured ingress logs to LLM and forwarded traffic when enabled, and expose the toggle in both settings and the logs page.
@SsuJojo SsuJojo marked this pull request as draft April 17, 2026 03:19
Move log-related controls into a dedicated settings drawer and keep the saved logs enabled setting in sync with the logs page toggle while leaving pause as an independent runtime state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants