Skip to content

Bound the prose: log lines and exception messages - #26

Open
VSN2015 wants to merge 1 commit into
masterfrom
fix/bound-human-readable-prose
Open

Bound the prose: log lines and exception messages#26
VSN2015 wants to merge 1 commit into
masterfrom
fix/bound-human-readable-prose

Conversation

@VSN2015

@VSN2015 VSN2015 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Third in the unbounded-work family (#24, #25). This one writes to your logs.

The bug

The unknown: :log warn line joined every undeclared key, and the violation summary behind InvalidParameters#message — also used for the monitor-mode warn line — joined every violation.

50,000 undeclared keys, unknown: :log:

Result
Before one 1,088,000-byte logger.warn
After one 261-byte logger.warn

The same request under unknown: :error produced a 1 MB exception message — which is what your error tracker receives, indexes and bills you for.

And unknown: :log is the mode the rollout documentation recommends ("the rollout-friendly middle ground"), so this sat on the recommended path, not in an exotic corner.

The fix

A log line and an exception message are prose, written for a person. They now list at most ten names and count the rest:

Permittable: unknown parameter(s) ignored by the #create contract: undeclared_key_0,
undeclared_key_1, ... undeclared_key_9, and 49990 more

What deliberately does not change

The machine-readable channels stay complete:

  • InvalidParameters#details still names every offender.
  • The invalid_parameters.permittable instrumentation payload still carries every violation.

Nothing should silently drop data a consumer might be reading, and a dashboard counting rejections needs the whole list. Only the sentence is bounded — a spec asserts details.length == 5_000 while the message is under 400 bytes.

The one visible change

The message string (and the problem-details detail under #19) when there are more than ten violations — which no ordinary contract reaches, since it needs more violations than a contract usually has fields. A spec pins that a single-violation message is byte-for-byte what it was:

expect(violations_for({ a: "nope" }, &decl).message).to eq("Invalid parameters: a (inclusion)")

Verification

  • 204 examples, 0 failures (5 new, written before the fix): the capped list, the uncapped list at ten or fewer, complete details alongside a bounded message, the monitor-mode line, and an ordinary message unchanged
  • RuboCop clean

@VSN2015

VSN2015 commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Code review — #26 Bound the prose

Verdict: mergeable with fixes. The count cap is correct and leaves every structured channel intact, but the PR's own guarantee ("one request cannot write a megabyte of log") is defeated by a single long key name, and unknown: :log now silently drops keys beyond the tenth with no other channel to find them.

Verified

  • bundle exec rspec at 2acaf0d: 204 examples, 0 failures. rubocop: no offenses.
  • 50,000 undeclared keys under unknown: :log → one 261-byte warn line (matches the CHANGELOG exactly). Boundary is right: 10 keys lists all ten, 11 keys lists ten plus ", and 1 more".
  • One 1,000,000-byte key name → a 1,000,067-byte warn line under :log, and a 1,000,030-byte exception message under :error.
  • unknown: :error, 50,000 keys → 314-byte message, details.length == 50000, 422 body is 2.37 MB.
  • i18n path is capped the same way; sensitive: values never enter the prose so redaction is unaffected.

Strengths

  • One helper (permittable_prose_list, lib/permittable.rb:852-857) feeds both the violation summary and the :log line, so the two cannot drift.
  • Structured channels are genuinely untouched: details (832-833), the notification payload (836-840) and the envelope (779-783) all carry every violation.
  • Tests use a captured logger and a real ActiveSupport::Notifications subscription, and each "bounded" example asserts the full structured channel in the same test (spec/permittable_spec.rb:644-704). Good byte-for-byte pin on the single-violation message (706-709).

Important

  1. Per-item length is unbounded (lib/permittable.rb:852-857). Only the count is capped. A 1 MB JSON key is trivial to send and produces the exact 1 MB log line / exception message README:324 says is now impossible. A violation's param path has the same exposure. Fix: truncate each item (e.g. item[0, 117] + "..." past 120 chars) or cap the joined string by bytes and count the remainder. Add a spec with one long key.
  2. unknown: :log loses keys beyond ten with no fallback (lib/permittable.rb:1013-1015). Under :log no violation is recorded and no event fires, so the warn line was the only record. The "machine-readable channels stay complete" argument holds for :error and monitor mode but not here, and :log is exactly the adoption case where undeclared-key counts are large. Since parser insertion order is stable per client, keys 11+ may never surface. Options: a higher limit for the :log line plus the byte cap from (1), a Permittable.prose_list_limit= setting, or an unknown_parameters.permittable event carrying the full list. At minimum, document the trade-off in the README :log row.
  3. No boundary test. spec/permittable_spec.rb:656-671 tests 5,000 and 3. A < in place of <= at line 854 would pass the suite. Add examples at exactly 10 and 11.

Minor

  • CHANGELOG.md:7 and the PR body quote "the rollout-friendly middle ground" as README text recommending :log. That phrase exists nowhere in the repo; the README rollout recipe recommends monitor. Reword.
  • The 422 body is still O(N) under unknown: :error (2.37 MB for 50,000 keys). That is deliberate given "details is complete", but after Make array length: a bound, not just a report #24 framed multi-MB bodies as the problem, one sentence saying so would pre-empt the question.
  • lib/permittable.rb:844-846 and 1015 map every item and then first(10) discards them. items.first(LIMIT).map(...) plus a separate count avoids the throwaway work.
  • The PR body's sample output shows a literal ... between items; the real line lists all ten names then ", and 49990 more". Cosmetic.

@VSN2015 VSN2015 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review of PR #26: Log and exception message bounding prevents log flooding and oversized payloads in exception tracking systems while preserving full structured details.

Comment thread lib/permittable.rb
# write a megabyte of them. The machine-readable channels — a violation's
# `details` and the instrumentation payload — stay complete; only the
# sentence is bounded.
PROSE_LIST_LIMIT = 10

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

PROSE_LIST_LIMIT = 10 is an ideal threshold for human-readable error summaries and log warnings.

Comment thread lib/permittable.rb
# See PROSE_LIST_LIMIT. `unknown: :error` on a request carrying 50,000
# undeclared keys used to produce a 50,000-item sentence — a megabyte of
# log line, or of exception message handed to every error tracker.
def permittable_prose_list(items)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Formatting with "\#{shown}, and \#{items.length - PROSE_LIST_LIMIT} more" keeps the message concise while details retains the complete set of parameters for machine consumers.

The unknown: :log warn line joined every undeclared key, and the
violation summary behind InvalidParameters#message — also used for the
monitor-mode warn line — joined every violation.

  50,000 undeclared keys, unknown: :log
  before: one 1,088,000-byte logger.warn
  after:  one 261-byte logger.warn

The same request under unknown: :error produced a 1 MB exception
message, which is what an error tracker receives and indexes. And
unknown: :log is the mode the rollout documentation recommends, so
this sat on the recommended path rather than in an exotic corner.

A log line and an exception message are PROSE, written for a person.
They now list at most ten names and count the rest ("..., and 49990
more").

What deliberately does NOT change: the machine-readable channels stay
complete. InvalidParameters#details still names every offender, and so
does the invalid_parameters.permittable payload — nothing should
silently drop data a consumer might be reading, and a dashboard
counting rejections needs the whole list. Only the sentence is
bounded.

The one visible change is the message / problem detail string when
there are more than ten violations, which no ordinary contract reaches;
a spec pins that a single-violation message is byte-for-byte what it
was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@VSN2015
VSN2015 force-pushed the fix/bound-human-readable-prose branch from 2acaf0d to 0383829 Compare September 11, 2026 21:38
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.

1 participant