Skip to content

feat(web): structured log display with filters in webui - #630

Merged
jdx merged 27 commits into
jdx:mainfrom
gaojunran:feat/webui-structured-logs
Jul 29, 2026
Merged

feat(web): structured log display with filters in webui#630
jdx merged 27 commits into
jdx:mainfrom
gaojunran:feat/webui-structured-logs

Conversation

@gaojunran

@gaojunran gaojunran commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Add structured log rendering to the web UI, matching the CLI's display style with level badges, logger names, and key=value field formatting.

Backend

  • Tail and SSE endpoints emit JSONL (application/x-ndjson) with include_structured: true
  • Extract From<LogEntry> for JsonLogEntry to centralize conversion
  • Filter query params: since, until, level, grep, regex, logger, case_sensitive, field (KEY=VALUE), jq
  • Reuse CLI's MessageFilter, FieldFilter, and JqFilter for filtering
  • New endpoints:
    • GET /api/logs/{id}/loggers — distinct logger names for dropdown
    • GET /api/logs/{id}/field-keys — distinct field keys for jq autocomplete
  • FieldFilter::LoggerContains variant with SQLite LIKE query
  • Pre-validate regex and jq expressions (400 on parse error)
  • Fix logfmt parser to reject non-logfmt lines (key character validation + bare key ratio check)

Frontend

  • StructuredLogEntry type and JSONL parsing in useLogStream
  • Structured rendering: level badge with CLI color mapping, logger italic dim, message bold, fields key=value with type-colored values
  • Filter bar: Level, Logger (dropdown from API), Search (with Regex/Case toggles), jq (with autocomplete from builtins + field keys), Since, Until
  • Responsive layout: 3-column grid on desktop, stacked on mobile
  • Fix Vue scoped style not applying to v-html content
  • Mobile timestamp: stacked date/time layout

Test plan

  • mise run ci-dev passes
  • Structured logs render correctly in webui (level badges, fields)
  • All filters work (level, logger, grep, regex, jq, since, until, case-sensitive)
  • jq autocomplete suggests builtins and field keys
  • Invalid jq/regex returns error feedback
  • Non-logfmt lines (Go standard log) not misparsed
  • Mobile layout: filters stack vertically, timestamp stacks

This PR was created by Claude Code.

Summary by CodeRabbit

  • New Features

    • Added interactive log filters for time range, level, logger, text, regular expressions, structured fields, case sensitivity, and jq expressions.
    • Added jq autocomplete suggestions and endpoints for available loggers and field keys.
    • Log streams and SSE output now use structured newline-delimited JSON.
    • Enhanced log display with level badges, logger labels, formatted fields, and responsive timestamps.
  • Bug Fixes

    • Improved logfmt parsing for bare keys, empty values, hyphenated keys, and standard log lines.
    • Preserved streaming progress when entries are filtered or log metadata is temporarily unavailable.

Convert nested if-let patterns to Rust 2024 let-chain syntax across
env, supervisor, pitchfork_toml, log_parse, log_store, cli, and web modules.
Add structured log rendering to the web UI, matching the CLI's display
style with level badges, logger names, and key=value field formatting.

Backend:
- Tail and SSE endpoints now emit JSONL (application/x-ndjson) with
  include_structured: true
- Extract From<LogEntry> for JsonLogEntry to centralize conversion
- Add filter query params: since, until, level, grep, regex, logger,
  case_sensitive, field (KEY=VALUE), jq expression
- Reuse CLI's MessageFilter, FieldFilter, and JqFilter for filtering
- Add GET /api/logs/{id}/loggers endpoint for distinct logger names
- Add GET /api/logs/{id}/field-keys endpoint for jq autocomplete
- Add FieldFilter::LoggerContains variant with SQLite LIKE query
- Add distinct_loggers() and distinct_field_keys() to SqliteLogStore
- Pre-validate regex and jq expressions, returning 400 on parse error
- Fix logfmt parser to reject non-logfmt lines (key character validation
  + bare key ratio check) so Go standard log format doesn't get
  misparse as logfmt

Frontend:
- Add StructuredLogEntry type and JSONL parsing in useLogStream
- Structured rendering in log.ts: level badge with CLI color mapping,
  logger italic dim, message bold, fields key=value with type-colored
  values
- Filter bar with Level, Logger (dropdown from API), Search (with Regex
  and Case-sensitive toggles), jq (with autocomplete from builtins +
  field keys), Since, Until datetime pickers
- Responsive layout: 3-column grid on desktop, stacked on mobile
- Fix Vue scoped style not applying to v-html content by moving
  v-html-related CSS to non-scoped style block
- Mobile timestamp: stacked date/time layout
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The pull request adds structured log serialization, filtered NDJSON streaming, SQLite logger and field-key discovery, and interactive UI filtering and rendering. It also improves logfmt parsing and consolidates existing conditional control flow across configuration, storage, and supervisor code.

Changes

Structured log flow

Layer / File(s) Summary
JSON log conversion and wiring
src/cli/json_output.rs, src/cli/logs.rs, src/cli/mod.rs, src/web/routes/logs.rs
LogEntry conversion is centralized in JsonLogEntry and reused by CLI and web log output, including structured fields.
Filtered log queries and NDJSON API
src/log_parse.rs, src/log_store/*, src/web/routes/api/logs.rs, src/web/server.rs
Logfmt parsing, structured field filters, logger filtering, discovery queries, datetime and regex validation, jq processing, cursor handling, and NDJSON tail responses are updated.
Typed stream parsing and log rendering
ui/src/types/api.ts, ui/src/composables/useApi.ts, ui/src/utils/log.ts
The UI receives typed structured entries, passes stream filters, parses NDJSON, and renders levels, loggers, messages, and custom fields.
Interactive log filters and presentation
ui/src/views/LogView.vue
LogView adds filter controls, jq suggestions, logger and field-key lookup, debounced filters, level styling, and responsive timestamp layouts.

Conditional control-flow cleanup

Layer / File(s) Summary
Combined-condition refactors
src/cli/settings.rs, src/config_types.rs, src/env.rs, src/pitchfork_toml.rs, src/supervisor/*, src/template.rs, src/log_store/sqlite.rs
Nested conditionals are consolidated across command, configuration, namespace, supervisor, lifecycle, watcher, template, and storage paths while preserving the described behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LogView
  participant TailAPI
  participant SqliteLogStore
  participant JsonLogEntry
  LogView->>TailAPI: request filters and stream logs
  TailAPI->>SqliteLogStore: query structured entries
  SqliteLogStore-->>TailAPI: return raw entries
  TailAPI->>JsonLogEntry: convert entries to JSON
  JsonLogEntry-->>LogView: emit NDJSON records
  LogView->>LogView: parse and render structured fields
Loading

Possibly related PRs

  • jdx/pitchfork#592: Adds structured log highlighting and rendering that connects to this PR’s structured log payloads.

Poem

I’m a rabbit hopping through logs in a stream,
With JSON carrots and filters supreme.
Fields bloom in colors, jq points the way,
NDJSON bubbles through night and day.
Thump-thump! The timestamps now shine—
Structured logs make this burrow divine.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: structured log display and filtering in the web UI.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds structured JSONL log streaming, server-side filtering and pagination, structured log rendering, and clear-generation synchronization.

  • Adds level, logger, text, time, field, regex, and jq filters.
  • Adds logger and field-key discovery endpoints.
  • Adds backward history loading and atomic clear-generation snapshots.
  • Improves logfmt handling for empty assignments, prefixed aliases, and non-logfmt lines.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failures remain within the scope of the previous review threads.

No blocking failure remains.

Important Files Changed

Filename Overview
src/web/routes/api/logs.rs Implements filtered JSONL history and live streaming with atomic generation snapshots and clear sentinels.
src/log_store/sqlite.rs Adds cursor pagination, parameterized structured-field matching, metadata discovery, and atomic query-generation reads.
src/log_parse.rs Distinguishes bare keys from explicit empty values and recognizes supported prefixed logfmt aliases.
ui/src/composables/useApi.ts Parses structured streams, loads older history, and rejects history snapshots invalidated by clear-generation changes.
ui/src/views/LogView.vue Adds structured log presentation, filtering controls, autocomplete, and responsive layouts.
ui/src/utils/log.ts Adds structured field formatting and escaped HTML rendering utilities.

Reviews (25): Last reviewed commit: "fix(web): use generation-based staleness..." | Re-trigger Greptile

Comment thread src/web/routes/api/logs.rs
Comment thread src/web/routes/api/logs.rs Outdated
Comment thread src/log_parse.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/log_store/sqlite.rs (1)

796-840: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unbounded full-history scans for autocomplete queries.

distinct_loggers and distinct_field_keys scan every row for a daemon (the latter also expanding each row via json_each), with no LIMIT and no supporting index on logger/fields_json. These are invoked from the new /loggers and /field-keys endpoints on every LogView mount/daemon switch, so cost scales with total log history rather than typical field-key cardinality.

Consider capping the scan (e.g. ORDER BY id DESC LIMIT N subquery before taking distinct values) or adding a (daemon_id, logger) index, since autocomplete only needs a representative sample, not the full history.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log_store/sqlite.rs` around lines 796 - 840, Bound the autocomplete work
in distinct_loggers and distinct_field_keys by limiting the log-entry sample
before applying DISTINCT and json_each, using an appropriate fixed cap while
preserving daemon filtering and sorted unique results. Avoid scanning the
daemon’s entire history on each request; keep both methods’ existing
Result<Vec<String>> behavior unchanged.
ui/src/views/LogView.vue (1)

140-159: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Initial log stream connection is needlessly delayed by 500ms.

The filter watcher uses { immediate: true }, but the callback body still always schedules the update via setTimeout(..., 500), so even the very first (no-op) filter evaluation on mount delays debouncedFilters — and therefore the initial useLogStream connection — by 500ms.

💡 Proposed fix: skip the debounce on the immediate/no-filter case
 watch(
   [filterLevel, filterLogger, filterSearch, filterSearchRegex, filterSearchCase, filterJq, filterSince, filterUntil],
-  () => {
+  (_, oldValues) => {
     if (debounceTimer) clearTimeout(debounceTimer)
-    debounceTimer = setTimeout(() => {
-      const search = filterSearch.value.trim()
-      debouncedFilters.value = {
-        level: filterLevel.value || undefined,
-        logger: filterLogger.value || undefined,
-        grep: search && !filterSearchRegex.value ? search : undefined,
-        regex: search && filterSearchRegex.value ? search : undefined,
-        caseSensitive: search && filterSearchCase.value ? true : undefined,
-        jq: filterJq.value.trim() || undefined,
-        since: filterSince.value || undefined,
-        until: filterUntil.value || undefined,
-      }
-    }, 500)
+    const apply = () => {
+      const search = filterSearch.value.trim()
+      debouncedFilters.value = {
+        level: filterLevel.value || undefined,
+        logger: filterLogger.value || undefined,
+        grep: search && !filterSearchRegex.value ? search : undefined,
+        regex: search && filterSearchRegex.value ? search : undefined,
+        caseSensitive: search && filterSearchCase.value ? true : undefined,
+        jq: filterJq.value.trim() || undefined,
+        since: filterSince.value || undefined,
+        until: filterUntil.value || undefined,
+      }
+    }
+    if (oldValues === undefined) {
+      apply() // first run on mount: apply immediately, no debounce
+    } else {
+      debounceTimer = setTimeout(apply, 500)
+    }
   },
   { immediate: true },
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/views/LogView.vue` around lines 140 - 159, Update the filter watcher
around debouncedFilters so the immediate initial evaluation with no active
filters applies the filter state synchronously instead of scheduling the 500ms
debounce. Preserve the existing debounce behavior for subsequent filter changes
and for an initial evaluation containing active filters, using the existing
filter values and debounceTimer flow.
ui/src/composables/useApi.ts (1)

218-234: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

LogStreamFilters doesn't expose the backend's field (KEY=VALUE) filter.

The backend tail endpoint's build_filters accepts repeated field query params for FieldFilter::FieldEq, and a /logs/{id}/field-keys endpoint was added specifically to support field-value filtering in the UI. LogStreamFilters/connect() here have no corresponding property, so this filter type can never be sent from the composable regardless of what LogView.vue wires up.

♻️ Suggested addition
 export interface LogStreamFilters {
   since?: string
   until?: string
   level?: string
   grep?: string
   regex?: string
   logger?: string
   jq?: string
   caseSensitive?: boolean
   lines?: number
+  fields?: Record<string, string>
 }
     if (f.caseSensitive) params.set('case_sensitive', 'true')
     if (f.jq) params.set('jq', f.jq)
+    if (f.fields) {
+      for (const [key, value] of Object.entries(f.fields)) {
+        params.append('field', `${key}=${value}`)
+      }
+    }
     if (f.lines !== undefined) params.set('lines', String(f.lines))

If field-value filtering is intentionally out of scope for this iteration, please disregard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/composables/useApi.ts` around lines 218 - 234, Extend LogStreamFilters
and the connect() query construction in useApi.ts to support field-value filters
as repeated `field` query parameters, preserving all existing filter handling.
Ensure the composable exposes the new field filter so LogView.vue wiring can
send each KEY=VALUE entry to the backend tail endpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/web/routes/api/logs.rs`:
- Around line 88-98: Validate each user-supplied field key in the web route’s
field-filter parsing before pushing FieldFilter::FieldEq, mirroring the
whitelist used by build_field_filters: allow only alphanumeric characters,
underscores, and periods. Ignore or reject entries with any other characters so
build_query_sql cannot receive an unsafe JSON path key.
- Around line 17-31: The TailQuery field cannot parse repeated query keys with
Axum’s standard Query extractor. Update the tail route’s query extraction and
TailQuery handling to use a repeated-value-compatible extractor or dependency,
preserving field=KEY=VALUE&field=KEY2=VALUE2 as a Vec<String> before
build_filters processes it.

---

Nitpick comments:
In `@src/log_store/sqlite.rs`:
- Around line 796-840: Bound the autocomplete work in distinct_loggers and
distinct_field_keys by limiting the log-entry sample before applying DISTINCT
and json_each, using an appropriate fixed cap while preserving daemon filtering
and sorted unique results. Avoid scanning the daemon’s entire history on each
request; keep both methods’ existing Result<Vec<String>> behavior unchanged.

In `@ui/src/composables/useApi.ts`:
- Around line 218-234: Extend LogStreamFilters and the connect() query
construction in useApi.ts to support field-value filters as repeated `field`
query parameters, preserving all existing filter handling. Ensure the composable
exposes the new field filter so LogView.vue wiring can send each KEY=VALUE entry
to the backend tail endpoint.

In `@ui/src/views/LogView.vue`:
- Around line 140-159: Update the filter watcher around debouncedFilters so the
immediate initial evaluation with no active filters applies the filter state
synchronously instead of scheduling the 500ms debounce. Preserve the existing
debounce behavior for subsequent filter changes and for an initial evaluation
containing active filters, using the existing filter values and debounceTimer
flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b940ec86-476b-43dc-ab91-95f703667587

📥 Commits

Reviewing files that changed from the base of the PR and between 26f8016 and 3943326.

📒 Files selected for processing (21)
  • src/cli/json_output.rs
  • src/cli/logs.rs
  • src/cli/mod.rs
  • src/cli/settings.rs
  • src/config_types.rs
  • src/env.rs
  • src/log_parse.rs
  • src/log_store/mod.rs
  • src/log_store/sqlite.rs
  • src/pitchfork_toml.rs
  • src/supervisor/lifecycle.rs
  • src/supervisor/mod.rs
  • src/supervisor/watchers.rs
  • src/template.rs
  • src/web/routes/api/logs.rs
  • src/web/routes/logs.rs
  • src/web/server.rs
  • ui/src/composables/useApi.ts
  • ui/src/types/api.ts
  • ui/src/utils/log.ts
  • ui/src/views/LogView.vue

Comment thread src/web/routes/api/logs.rs
Comment thread src/web/routes/api/logs.rs
@github-actions

Copy link
Copy Markdown
Contributor

This PR currently has failing checks. If this continues for 7 days, it will be closed automatically.

This is warning day 1 of 7.

Please update the PR when you have a chance. Feel free to reopen or create a new PR if it is closed and you'd like to continue working on it.

This comment was generated by an automated workflow.

@github-actions

Copy link
Copy Markdown
Contributor

This PR currently has failing checks. If this continues for 7 days, it will be closed automatically.

This is warning day 2 of 7.

Please update the PR when you have a chance. Feel free to reopen or create a new PR if it is closed and you'd like to continue working on it.

This comment was generated by an automated workflow.

…plain-text daemons

Backend:
- Validate field keys in build_filters (alphanumeric/underscore/period only)
- Return 400 on invalid level value instead of silently ignoring
- Return 400 on invalid since/until datetime instead of silently broadening
- Keep last_clear_gen on generation lookup error to prevent log replay
- Add LIMIT 5000 to distinct_loggers and distinct_field_keys queries
- Replace unwrap_or_default() on serde_json::to_string with filter_map/ok
- Add tests for logfmt bare key ratio edge cases and Go standard log rejection

Frontend:
- Skip 500ms debounce on initial mount, apply filters immediately
- Hide Level/Logger/jq filters when daemon has no structured logs
  (determined by empty availableLoggers and jqFieldKeys)

Format:
- cargo fmt normalization on let-chain syntax from previous commit
Comment thread src/web/routes/api/logs.rs Outdated
Comment thread src/log_parse.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/log_parse.rs (1)

222-228: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve whether a pair used = before applying the heuristic.

Bare foo and explicit foo="" both become an empty string, so valid logfmt containing empty values can be rejected as mostly bare keys. Track an explicit key/value flag (or quoted-value flag) while parsing instead of inferring it from v.is_empty().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/log_parse.rs` around lines 222 - 228, Update the pair parsing and
heuristic in the log parsing flow around the `proper` calculation to preserve
whether each pair explicitly used `=` (including quoted empty values). Count
that recorded key/value indicator rather than inferring validity from
`v.is_empty()`, so bare keys remain distinguishable from valid empty-value
pairs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/log_parse.rs`:
- Around line 175-177: Update the invalid key=value handling in the log parsing
loop so that when validation fails with i at the equals delimiter, it consumes
the delimiter and skips the associated value through the end of that token
instead of parsing it as a bare key; alternatively reject the line. Preserve
normal parsing for subsequent valid tokens such as level=info.
- Around line 171-174: Align the key grammar used by parse_logfmt_pairs with the
web API field validation: either remove hyphen support from the parser predicate
or consistently allow hyphens in the API filter grammar. Prefer a shared
predicate where practical, and add a cross-layer validation test covering
request-id to prevent future mismatches.

---

Outside diff comments:
In `@src/log_parse.rs`:
- Around line 222-228: Update the pair parsing and heuristic in the log parsing
flow around the `proper` calculation to preserve whether each pair explicitly
used `=` (including quoted empty values). Count that recorded key/value
indicator rather than inferring validity from `v.is_empty()`, so bare keys
remain distinguishable from valid empty-value pairs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 74860d3a-5e0d-4f41-9c38-5cc4ea2fd13f

📥 Commits

Reviewing files that changed from the base of the PR and between 3943326 and 8dc0d38.

📒 Files selected for processing (13)
  • src/cli/settings.rs
  • src/config_types.rs
  • src/env.rs
  • src/log_parse.rs
  • src/log_store/sqlite.rs
  • src/pitchfork_toml.rs
  • src/supervisor/lifecycle.rs
  • src/supervisor/mod.rs
  • src/supervisor/watchers.rs
  • src/template.rs
  • src/web/routes/api/logs.rs
  • src/web/server.rs
  • ui/src/views/LogView.vue
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/web/server.rs
  • src/config_types.rs
  • src/pitchfork_toml.rs
  • src/template.rs
  • src/supervisor/lifecycle.rs
  • src/supervisor/watchers.rs
  • src/supervisor/mod.rs
  • ui/src/views/LogView.vue
  • src/cli/settings.rs
  • src/log_store/sqlite.rs
  • src/web/routes/api/logs.rs

Comment thread src/log_parse.rs
Comment thread src/log_parse.rs Outdated
- Fix dotted field key matching: FieldEq now uses json_each with
  parameterized key instead of json_extract path interpolation, which
  treated '.' as nested traversal (e.g. request.id never matched)
- Fix invalid key token handling: skip entire =value when key validation
  fails, not just 1 byte
- Fix bare key vs empty value distinction: use Option<String> (None =
  bare key, Some("") = explicit empty) so ratio check doesn't misclassify
- Align is_safe_field_key with parser: add hyphen support
- Add tests: explicit empty value, hyphenated key, Go standard log rejection

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/log_store/sqlite.rs`:
- Around line 625-638: Update the field-filter condition construction in the
query-building logic around query_params to compare values according to
json_each.type rather than always comparing json_each.value with text
parameters. Normalize or bind CLI filter values so boolean, null, and numeric
filters match their corresponding JSON scalar types while preserving string and
dotted-key handling. Add tests covering typed filters such as ready=true and
val=null, including the expected matches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b82f7ddc-77fd-4542-83df-b515b6ea147f

📥 Commits

Reviewing files that changed from the base of the PR and between 8dc0d38 and 25abe71.

📒 Files selected for processing (3)
  • src/log_parse.rs
  • src/log_store/sqlite.rs
  • src/web/routes/api/logs.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/web/routes/api/logs.rs

Comment thread src/log_store/sqlite.rs
Comment thread src/web/routes/api/logs.rs Outdated
Comment thread src/log_store/sqlite.rs
Comment thread src/log_store/sqlite.rs Outdated
Comment thread src/web/routes/api/logs.rs Outdated
- Convert text filter values to JSON literals via text_to_json_literal()
  so json_each.value (native SQLite types) matches correctly: numbers,
  booleans, and null now work with --field port=8080, ready=true, val=null
- Use IS instead of = for NULL comparison (NULL = NULL is false in SQL)
- Return 400 when field param lacks '=' instead of silently ignoring
- Align error message to include hyphen in allowed characters
@gaojunran
gaojunran force-pushed the feat/webui-structured-logs branch from d547634 to 683fe31 Compare July 25, 2026 09:40
Comment thread src/log_store/sqlite.rs Outdated
json(?) returns JSON text (with quotes for strings), which doesn't
match json_each.value's native SQLite type. Switch to
json_extract(?, '$') which extracts the native value from JSON text,
so string/number/boolean/null comparisons all work correctly.
Comment thread src/web/routes/api/logs.rs Outdated
Comment thread src/log_store/sqlite.rs Outdated
Comment thread src/log_store/sqlite.rs Outdated
- Add before_id backward pagination to LogQuery and SQLite query layer
- Add id field to JsonLogEntry (skip_serializing_if zero, no CLI impact)
- Tail endpoint returns one-shot JSONL when before_id is set (no polling)
- Frontend: loadMoreHistory() fetches 100 older entries on scroll-to-top
- Frontend: scroll position preserved during prepend, loading spinner
- Frontend: hasMoreHistory flag stops loading when fewer than 100 returned
- Remove clear filter button (activeFilterCount badge remains on Filter toggle)
Comment thread src/log_store/sqlite.rs Outdated
Comment thread src/log_store/sqlite.rs Outdated
- cargo fmt: normalize after_id/before_id struct field formatting
  across logs.rs, mcp.rs, log_store/mod.rs, tui/app.rs, web routes
  (fixes CI build failure)

- Fix text_to_json_literal numeric validation: replace f64::parse with
  serde_json::from_str so Rust-valid-but-invalid-JSON numbers like
  "+42", "1.", ".5" are treated as strings (quoted) instead of
  causing SQLite json_extract to fail and stall the log stream

- Fix initial clear generation lookup: on transient failure, store None
  instead of 0 so the first successful poll records the real generation
  without treating it as a clear event (which would reset the cursor
  and replay the entire log history)

- Add unit tests for text_to_json_literal covering booleans, null,
  valid JSON numbers, invalid JSON number spellings, and strings
HTML datetime-local input defaults to 'YYYY-MM-DDTHH:MM' (no seconds),
but parse_datetime only tried formats with seconds, causing a 400 error
when only 'since' is passed. Add two no-seconds format patterns.
- Fix jq field key suggestions: jqFieldKeys are keys inside the .fields
  object, not top-level keys. Previously they were mixed with
  topLevelKeys at root level, so typing '.' suggested '.event_type'
  (wrong — should be '.fields.event_type'). Now:
  - Root context (.) → suggest only topLevelKeys (timestamp, fields, etc)
  - Fields context (.fields.) → suggest only jqFieldKeys
  - Deeper paths → no suggestions (avoids wrong guesses)

- Increase jq dropdown opacity from rgba(255,255,255,0.03) (nearly
  transparent) to rgba(18,18,21,0.96) (nearly opaque) with a stronger
  border and drop shadow for readability
Comment thread src/log_store/sqlite.rs
…itive

Field filter values were always converted to typed JSON literals (number,
boolean, null) before comparison with json_each.value. This made string
fields storing text like "true", "42", or "null" unreachable via
field filters, since json_each.value (TEXT) would never match
json_extract('true', '$') (INTEGER 1).

Add a raw text comparison path alongside the typed path. Since
json_each.value has no type affinity, the text IS comparison only
matches string values, not native integers or booleans, so there are
no false positives.
Comment thread src/web/routes/api/logs.rs Outdated
When a daemon has never been cleared, log_clear_generations has no row
and initial_gen is None. The None branch treated the first successful
poll as a clear event, resetting the cursor to 0 and replaying the
entire initial history as duplicates in the append-only stream.

Now that query_with_generation does both reads atomically, None only
means 'never cleared' — a transient failure fails the whole call and
returns an error response, never None. Initialize last_clear_gen to
initial_gen.unwrap_or(0) and remove the None branch entirely. A
never-cleared daemon has generation 0 on both initial fetch and first
poll, so no spurious clear is detected.
Comment thread src/web/routes/api/logs.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

This PR currently has failing checks. If this continues for 7 days, it will be closed automatically.

This is warning day 1 of 7.

Please update the PR when you have a chance. Feel free to reopen or create a new PR if it is closed and you'd like to continue working on it.

This comment was generated by an automated workflow.

The polling loop read the clear generation and new rows in two separate
spawn_blocking calls. A clear could interleave after the generation
check but before the row query, producing post-clear rows that the next
poll would replay when it detected the generation change and reset the
cursor to 0.

Use query_with_generation for the poll query too, so both reads share a
single transaction snapshot. If a clear happened, the generation check
in the same atomic batch catches it and skips the rows, preventing
duplicates.
Comment thread src/web/routes/logs.rs Outdated
The SSE endpoint had the same clear race as the NDJSON endpoint: the
clear generation and incremental rows were read in separate
spawn_blocking calls, allowing a clear to interleave and produce
duplicate streamed entries.

Use query_with_generation for both the initial cursor capture and the
poll loop, so each read pair shares a single transaction snapshot.
Comment thread src/web/routes/logs.rs Outdated
Comment thread src/web/routes/api/logs.rs
Three issues across the streaming paths:

1. SSE init failure replay (src/web/routes/logs.rs): when the initial
   query_with_generation failed, the code fell back to (last_id=0,
   last_clear_gen=0). The next successful poll then queried after_id=0,
   replaying the entire log history. Now yields an error event and
   returns instead.

2. NDJSON clear signal missing (src/web/routes/api/logs.rs): when a
   clear was detected during polling, the backend reset its cursor and
   skipped the batch, but the frontend never learned about the clear.
   Old entries remained in the display buffer permanently. Now emits a
   {"_clear":true} sentinel line so the frontend can flush.

3. Frontend clear handling (ui/src/composables/useApi.ts): the NDJSON
   stream parser now recognizes the _clear sentinel and clears the
   lines buffer, removing stale entries.
Comment thread src/web/routes/api/logs.rs Outdated
is_safe_field_key restricted field filter keys to [a-zA-Z0-9_.-], the
logfmt parser charset. But field-keys discovery returns keys extracted
via json_each, which can contain any valid JSON key character (e.g.
@timestamp, http.status, keys with spaces). The store query uses
fully parameterized SQL (json_each.key = ?), so arbitrary keys are
safe and the whitelist only blocked valid discoverable fields.

Remove the whitelist, keep only a non-empty check.
@gaojunran

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread src/log_parse.rs Outdated
@gaojunran
gaojunran force-pushed the feat/webui-structured-logs branch from 46245d9 to c31baf7 Compare July 28, 2026 14:16
aube v1.8.0 had no GitHub artifact attestations, causing mise 2026.7.15
to fail attestation verification in CI. Regenerating the lockfile with
--bump re-resolves the 'latest' selector to aube 1.34.0, which has
valid attestations. Other tools also advance: communique 1.2.4,
cargo-nextest 0.9.140, cargo-binstall 1.21.1, node 24.15.0.
@gaojunran
gaojunran force-pushed the feat/webui-structured-logs branch from c31baf7 to 002b1d4 Compare July 28, 2026 14:39
The logfmt tokenizer rejected keys containing '@', but extract_level
looks for '@Level' as a level field name. So log lines using pino/syslog
style (@Level=info @message=hello) had their @Level key discarded before
level extraction could process it, losing the level and causing the
entry to disappear from level-filtered results.

Add '@' to the logfmt key charset alongside the existing alphanumeric,
'_', '.', '-' characters.
Comment thread src/web/routes/api/logs.rs
…path

Add comment explaining that execute_built_query is called directly
instead of self.query() to avoid query_parallel opening separate
connections outside the transaction, which would break the WAL
snapshot guarantee.

@gaojunran gaojunran left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@greptileai This is the same concern raised in the previous review (discussion_r3667516249) and already addressed there.

query_with_generation does not call self.query(). It calls build_query_sql + execute_built_query directly on the locked transaction connection (sqlite.rs:1209-1210). The parallel path (query_parallel) is only reachable through self.query() (sqlite.rs:1057-1064), which query_with_generation never invokes. Both the history query and the generation read execute on the same connection within the same BEGIN/ROLLBACK transaction, sharing one WAL snapshot. No separate connections are opened.

The invariant is documented with a comment (commit 6f01d5d) to prevent future refactors from accidentally introducing this race.

This comment was generated by Claude Code.

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

You're right, and I apologize for the repeated concern. The implementation is clear:

  • query_with_generation acquires the mutex lock once, executes BEGIN, then calls build_query_sql + execute_built_query directly on that locked connection — never self.query()
  • The generation read follows on the same connection within the same WAL snapshot
  • The comment at line ~1204 explicitly documents the invariant to guard against future refactors

The snapshot consistency is properly guaranteed. I should have recognized this was already addressed before raising it again.

@gaojunran

Copy link
Copy Markdown
Contributor Author

@greptileai

Resolve mise.lock conflict by regenerating from scratch with mise lock
--bump. aube resolves to 1.34.0 with valid attestations.
@gaojunran
gaojunran marked this pull request as ready for review July 29, 2026 18:06
Comment thread src/web/routes/api/logs.rs
When loadMoreHistory fires a before_id request and a log clear happens
while that request is in-flight, the clear sentinel empties the buffer
but the paginated response arrives afterward with pre-clear entries and
unconditionally unshifts them back, permanently restoring deleted logs.

Track a clearEpoch counter incremented on each clear sentinel.
loadMoreHistory captures the epoch before fetching and discards results
if the epoch changed, ensuring pre-clear entries are never merged back.
Comment thread ui/src/composables/useApi.ts Outdated
The clearEpoch counter only incremented when the stream received the
clear sentinel. If the stream disconnected before delivering it (or
the backward-history response arrived before the sentinel), stale
pre-clear entries were merged back, permanently restoring deleted logs.

Replace clearEpoch with streamGen, tracking the actual clear generation:
- Backend: x-log-generation header on both initial stream and before_id
  responses; clear sentinel now includes _gen value
- Frontend: streamGen updated from initial response header and clear
  sentinels; loadMoreHistory compares response generation against
  streamGen and discards if older (clear happened) or if streamGen
  changed during fetch (disconnect/reconnect with newer generation)
@jdx
jdx merged commit 063a220 into jdx:main Jul 29, 2026
15 checks passed
@jdx jdx mentioned this pull request Jul 29, 2026
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