Skip to content

feat(audit): record ingress rewriters that changed nothing - #594

Merged
SantiagoDePolonia merged 2 commits into
mainfrom
feat/audit-no-change-rewrite-steps
Jul 26, 2026
Merged

feat(audit): record ingress rewriters that changed nothing#594
SantiagoDePolonia merged 2 commits into
mainfrom
feat/audit-no-change-rewrite-steps

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Why

The ingress request-rewrite chain only wrote an audit revision when a rewriter returned a new body. So in the audit log UI, "compression ran and found nothing to dedupe" and "compression never ran at all" look identical — there is simply no step. That ambiguity is what prompted this: real traffic with no repeated line runs produces no compression tab, and the only way to tell the difference was to check response headers or the DB.

What

  • RequestRevisionSnapshot gains NoChange bool. Rewriters that run and leave the body alone now get a snapshot recorded with BytesAfter == BytesBefore, no body and no savings. The flag is positive rather than negated so pre-existing entries still read correctly: an old revision always changed the body.
  • request_rewrite.go records the step on both no-op paths (res == nil and res.Body == nil), and still applies any response headers such a rewriter set. Byte counts measure the current body, so a no-op after an earlier rewrite reports the rewritten size.
  • Dashboard: no-change revisions are excluded from the tab strip — they are steps, not versions of the request — and render as a quiet dashed pill on the Request tab (pro-token-compression: no change). Tab numbering is unchanged because panes are built from the changed revisions only.
  • Regenerated docs.go / openapi.json and the embedded dashboard bundle.

Testing

  • TestRequestRewriteMiddlewareRecordsNoChangeRevisions covers a mixed chain (no-op → body swap → header-only rewriter): order, the flag, equal byte sizes, nil body, header still applied, and that a trailing no-op measures the post-rewrite size.
  • Dashboard unit tests cover no-change-only, mixed, and legacy (flag absent) entries.
  • go test ./internal/server/... ./internal/auditlog/... and the 361-test dashboard suite pass; all pre-commit hooks green, including the dist-in-sync check.
  • Verified live against a pro build: a compressible request records tokens_saved: 430, an incompressible one records {"rewriter":"pro-token-compression","bytes_before":1494,"bytes_after":1494,"no_change":true}.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Audit logs now record every request rewrite step, including no-op rewriters, and track them with a new “no change” flag.
    • Dashboard UI now surfaces “no-change” steps as compact pills, while revision tabs show only body-changing rewrites.
    • Updated OpenAPI/Swagger documentation to reflect revised audit semantics.
  • Bug Fixes

    • Response headers emitted by no-op rewrite steps are now preserved.
    • Forwarded payload selection matches the last body-changing revision (or falls back when none changed).
  • Tests

    • Added coverage for correct audit entries and dashboard rendering for no-change/mixed rewrite scenarios.

The request-rewrite chain only recorded a revision when a rewriter
returned a new body, so an operator looking at an audit entry could not
tell "compression ran and found nothing to dedupe" apart from
"compression never ran at all" — the two look identical in the UI.

Rewriters that run and leave the body alone now get a revision snapshot
flagged NoChange (BytesAfter == BytesBefore, no body, no savings). The
flag is positive rather than negated so entries written before this
change still read correctly: an old revision always changed the body.

The dashboard keeps these out of the tab strip — they are steps, not
versions of the request — and renders them as a quiet dashed pill on the
Request tab instead. Tab numbering is unaffected: panes are built from
the changed revisions only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 26, 2026 19:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 60b93723-a356-49ec-90d6-b6e0b42f4382

📥 Commits

Reviewing files that changed from the base of the PR and between d53c67e and 8b042d7.

📒 Files selected for processing (5)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/auditlog/auditlog.go
  • internal/server/request_rewrite.go
  • internal/server/request_rewrite_test.go

📝 Walkthrough

Walkthrough

Request rewrite auditing now records rewriters that leave the body unchanged, adds the no_change audit field, and displays those steps as dashboard pills while creating revision panes only for changed bodies.

Changes

Request rewrite audit tracking

Layer / File(s) Summary
Audit revision contract
internal/auditlog/auditlog.go, cmd/gomodel/docs/docs.go, docs/openapi.json
Adds NoChange to request revision snapshots and documents entries for every rewriter that ran, including unchanged steps.
Middleware recording and validation
internal/server/request_rewrite.go, internal/server/request_rewrite_test.go
Records unchanged revisions, preserves response headers, updates body state only for non-nil bodies, and tests ordering and byte measurements.
Dashboard revision rendering
web/dashboard/src/pages/audit-logs/*, web/dashboard/tests/audit-list.js
Separates changed revisions from no-change steps, renders no-change pills, and covers mixed and legacy entries.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RequestRewriteMiddleware
  participant AuditLog
  participant Dashboard
  Client->>RequestRewriteMiddleware: submit request
  RequestRewriteMiddleware->>AuditLog: record changed and no-change revisions
  AuditLog-->>Dashboard: expose request revision data
  Dashboard-->>Dashboard: render changed panes and no-change pills
Loading

Possibly related PRs

Suggested reviewers: copilot

Poem

I hopped through rewrites, both changed and still,
Logged every step with a careful quill.
No-change pills now softly glow,
While changed panes show the flow.
The audit trail grows neat and bright—
A bunny-reviewed request delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly describes recording audit rewriters that make no body change.
Description check ✅ Passed The description covers why, what, and testing, so it is mostly complete despite using custom headings instead of the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/audit-no-change-rewrite-steps

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.

@mintlify

mintlify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 26, 2026, 7:02 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@cmd/gomodel/docs/docs.go`:
- Line 8064: Update the RequestRevisions description annotation to state that
when all rewriters are no-ops, the original request body is forwarded because no
last changed revision exists, and document the no_change invariants: bytes_after
equals bytes_before, no body is captured, and savings is zero. Then regenerate
the generated OpenAPI documentation artifact.

In `@internal/server/request_rewrite_test.go`:
- Around line 440-501: The TestRequestRewriteMiddlewareRecordsNoChangeRevisions
test should use table-driven subtests to cover nil-result, header-only/nil-body,
and body-rewrite rewriter behaviors. Refactor the existing inline rewriters and
assertions into behavior-focused table entries while preserving verification of
revision metadata, response headers, body sizes, and savings.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 338acd19-f9c3-40ff-bf9c-1e3b0b339eff

📥 Commits

Reviewing files that changed from the base of the PR and between 567aa81 and d53c67e.

⛔ Files ignored due to path filters (3)
  • internal/admin/dashboard/static/dist/assets/index-BuCjMNNr.css is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/assets/index-D19L8xXa.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (8)
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/auditlog/auditlog.go
  • internal/server/request_rewrite.go
  • internal/server/request_rewrite_test.go
  • web/dashboard/src/pages/audit-logs/AuditPaneTabs.svelte
  • web/dashboard/src/pages/audit-logs/audit-logic.js
  • web/dashboard/tests/audit-list.test.js

Comment thread cmd/gomodel/docs/docs.go Outdated
Comment on lines +440 to +501
func TestRequestRewriteMiddlewareRecordsNoChangeRevisions(t *testing.T) {
// A rewriter that inspects the request and forwards it untouched is
// still a step operators need to see, so it gets a no-change revision.
quiet := &stubRewriter{name: "quiet"}
// Response headers without a body change (a rewriter annotating why it
// did nothing) must not turn the step into a real revision.
annotating := &stubRewriter{
name: "annotating",
rewrite: func(ext.Input) (*ext.Result, error) {
header := http.Header{}
header.Set("X-Test-Rewriter", "skipped")
return &ext.Result{ResponseHeader: header}, nil
},
}

auditLogger := &capturingAuditLogger{config: auditlog.Config{Enabled: true, LogBodies: true}}
srv := New(newRewriteTestProvider(), &Config{
AuditLogger: auditLogger,
RequestRewriters: []ext.RequestRewriter{
quiet,
replaceBodyRewriter("swap", "PING", "PONG"),
annotating,
},
})
rec := postJSON(t, srv, "/v1/chat/completions",
`{"model":"gpt-4o-mini","messages":[{"role":"user","content":"PING"}]}`)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d (%s)", rec.Code, rec.Body.String())
}
if rec.Header().Get("X-Test-Rewriter") != "skipped" {
t.Error("response headers from a no-change rewriter must still be applied")
}
if len(auditLogger.entries) == 0 {
t.Fatal("expected an audit entry")
}

revisions := auditLogger.entries[0].Data.RequestRevisions
if len(revisions) != 3 {
t.Fatalf("expected 3 revisions (2 no-change + 1 rewrite), got %d: %+v", len(revisions), revisions)
}
for i, want := range []struct {
rewriter string
noChange bool
}{{"quiet", true}, {"swap", false}, {"annotating", true}} {
got := revisions[i]
if got.Seq != i+1 || got.Rewriter != want.rewriter || got.NoChange != want.noChange {
t.Errorf("revision %d = %+v, want rewriter %q no_change=%v", i+1, got, want.rewriter, want.noChange)
}
}

quietRev := revisions[0]
if quietRev.BytesBefore == 0 || quietRev.BytesAfter != quietRev.BytesBefore {
t.Errorf("no-change revision must report equal sizes: %+v", quietRev)
}
if quietRev.Body != nil || quietRev.TokensSaved != 0 {
t.Errorf("no-change revision must carry no body or savings: %+v", quietRev)
}
// The trailing no-change step sees the body the previous rewriter produced.
if revisions[2].BytesBefore != revisions[1].BytesAfter {
t.Errorf("no-change revision must measure the current body: %+v", revisions[2])
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Make the new behavior coverage table-driven.

Model the nil-result, header-only/nil-body, and body-rewrite cases as table entries/subtests. As per coding guidelines, “Add or update table-driven tests for behavior changes.”

🤖 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 `@internal/server/request_rewrite_test.go` around lines 440 - 501, The
TestRequestRewriteMiddlewareRecordsNoChangeRevisions test should use
table-driven subtests to cover nil-result, header-only/nil-body, and
body-rewrite rewriter behaviors. Refactor the existing inline rewriters and
assertions into behavior-focused table entries while preserving verification of
revision metadata, response headers, body sizes, and savings.

Source: Coding guidelines

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

The PR is safe to merge, with a non-blocking audit-detail preservation issue.

No-change revisions correctly preserve execution order, byte counts, headers, and dashboard behavior, but structured detail returned alongside a nil body is omitted from the new audit snapshot.

Files Needing Attention: internal/server/request_rewrite.go

T-Rex T-Rex Logs

What T-Rex did

  • The runtime validation for TestRequestRewriteMiddlewareRecordsNoChangeRevisions completed successfully and reported exit code 0, confirming the package passed.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

sequenceDiagram
  participant C as Client
  participant M as Rewrite middleware
  participant R as Request rewriter
  participant A as Audit log
  participant D as Dashboard
  C->>M: Request body
  M->>R: Rewrite current body
  alt Body changed
    R-->>M: New body and metadata
    M->>A: Changed revision with body
    M->>M: Continue with rewritten body
    A-->>D: Render revision tab
  else No body change
    R-->>M: nil result or nil body
    M->>A: NoChange revision with equal byte counts
    A-->>D: Render no-change pill
  end
Loading

Reviews (1): Last reviewed commit: "feat(audit): record ingress rewriters th..." | Re-trigger Greptile

}
if res == nil || res.Body == nil {
// The rewriter ran and left the request alone. Record the
// step anyway so the audit trail distinguishes "compression

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 No-change detail is discarded

When a rewriter returns a nil body with structured Detail, this branch records an unchanged revision without passing that detail, preventing the audit trail from explaining why the rewriter made no change.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

A rewriter can return a result with no body but a structured Detail —
that is where it explains why it left the request alone. The no-change
snapshot dropped it, so the audit trail recorded that the step ran but
not what it concluded. Detail now carries over when the rewriter
returned a result; a rewriter that declined outright (nil result) still
has nothing to record.

Also documents the all-no-op case on RequestRevisions (no last changed
revision means the original body was forwarded) and the no-change
invariants on the flag itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 26, 2026 19:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SantiagoDePolonia
SantiagoDePolonia merged commit f46b955 into main Jul 26, 2026
20 checks passed
@mintlify

mintlify Bot commented Jul 26, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟡 Building Jul 26, 2026, 7:02 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

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.

3 participants