Skip to content

feat(logging): FIZZY-3212: filter sensitive arguments from service-call logging (v0.7.0) - #53

Merged
sebscholl merged 4 commits into
mainfrom
feature/fizzy-3212-filter-sensitive-args-from-logging
Aug 15, 2026
Merged

feat(logging): FIZZY-3212: filter sensitive arguments from service-call logging (v0.7.0)#53
sebscholl merged 4 commits into
mainfrom
feature/fizzy-3212-filter-sensitive-args-from-logging

Conversation

@adriantaut

@adriantaut adriantaut commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Problem

Servus::Support::Logger.log_call logs every service call's arguments verbatim (args.inspect). Any app passing credentials as service args leaks them into logs. Observed live in Machina Console's production CloudWatch: raw ps_ session bearer tokens on every request (Sessions::Resolve) and full Google OAuth auth hashes (access + id tokens) from Sessions::CreateFromOmniauth.

Fizzy: https://fizzy.internal.zarhq.dev/0000001/cards/3212

Change

  • Logger.log_call masks argument values matching Servus.config.log_filter_parameters via ActiveSupport::ParameterFilter before logging → {token: "[FILTERED]"}.
  • log_filter_parameters is a pure, opt-in config option: it defaults to [] (no filtering), and Servus never touches it after boot — whatever the app sets in config/initializers/servus.rb is authoritative. Accepts all ParameterFilter notations (partial-match strings/symbols, regexps, procs). With the default empty list, log_call short-circuits, so unconfigured behavior is byte-identical to 0.6.0.
  • The recommended defaults live at the app level, not the gem level: the documented initializer (site/rails/configuration.md) sets the option to Rails' stock credential list (passw email secret token _key crypt salt certificate otp ssn cvv cvc), and site/features/logging.md explains the option. Rails users can simply reuse their app's request-log filtering: config.log_filter_parameters = Rails.application.config.filter_parameters.
  • The ActiveSupport::ParameterFilter is memoized on Config and invalidated by the writer. The assigned list is dup'd and frozen, so in-place mutation (<<) raises FrozenError instead of silently drifting from the memoized filter — reconfiguring means assigning a new list. Runtime mutation of filter params is explicitly not a supported contract.
  • Version 0.7.0 + CHANGELOG entry.

Design revision

An earlier revision of this PR shipped a gem-level deny-list default and a railtie config.after_initialize hook that merged the app's filter_parameters into it. That hook ran after all app initializers, so it silently clobbered anything set in config/initializers/servus.rb, and the deny-list was impossible to opt out of (args named e.g. author or token_count would always log as [FILTERED]). Both are removed — the gem imposes nothing; the defaults are configuration, documented in the initializer example.

Adversarial review

Codex challenge review ran against the original diff; both findings addressed:

  1. High — railtie originally replaced the default with the app list (empty app list would disable filtering). Superseded: the railtie hook is now removed entirely.
  2. Medium — memoization compared against the same array reference (in-place mutation never rebuilt). Superseded: the logger's change-polling cache is removed entirely; memoization lives on Config and the writer invalidates it, with the frozen list making in-place mutation an error.

Testing

  • spec/servus/support/logger_spec.rb (7 examples): verbatim logging by default; with a configured list — value filtering, partial-match keys (raw_token, password), wholesale nested filtering, non-matching keys stay visible, reassignment applied on the next call; arbitrary custom keys (wand, sigil) masked as [FILTERED] with key names kept visible.
  • spec/servus/config_spec.rb (#log_filter_parameters, 4 examples): empty default, customization, frozen-list contract (<< raises FrozenError), memoized filter rebuilt on reassignment.
  • Full suite: 789 examples, 0 failures; rubocop clean.

Release

After merge: publish GitHub Release v0.7.0 (triggers the RubyGems publish workflow). Machina Console then bumps 0.2.1 → 0.7.0 (FIZZY-3211), adds config.log_filter_parameters to its config/initializers/servus.rb (filtering is opt-in — without it, args log verbatim), and deletes its app-level override initializer.

🤖 Generated with Claude Code

…ll logging

Logger.log_call logged service arguments verbatim, leaking credentials
passed as service args (session tokens, OmniAuth hashes, passwords) into
application logs — observed live in Machina Console's CloudWatch.

Filter args through ActiveSupport::ParameterFilter before logging. New
Servus.config.log_filter_parameters defaults to a credential-shaped
deny-list; in Rails apps the railtie merges the app's filter_parameters
into that default after boot, so gem filtering matches request-log
filtering without configuration. The filter is memoized against a copied
snapshot so in-place mutations of the list rebuild it. Version 0.7.0.
@adriantaut
adriantaut requested a review from sebscholl August 14, 2026 14:14
sebscholl and others added 3 commits August 15, 2026 10:42
…g option

The railtie's after_initialize hook forced the gem's deny-list: it ran
after all app initializers and unconditionally reassigned
log_filter_parameters, silently clobbering anything set in
config/initializers/servus.rb and making the default keys impossible to
opt out of (e.g. args named author or token_count always [FILTERED]).

Now the gem imposes nothing:

- log_filter_parameters defaults to [] (no filtering); unconfigured
  behavior is identical to 0.6.0 via a short-circuit in log_call
- railtie merge hook and DEFAULT_LOG_FILTER_PARAMETERS removed
- recommended defaults (Rails' stock credential list) documented in the
  initializer example in site/rails/configuration.md; Rails users can
  assign Rails.application.config.filter_parameters directly
- specs reworked for opt-in semantics, plus coverage that arbitrary
  configured keys are masked as [FILTERED] with key names kept visible

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… Config

The logger polled the config on every log call — holding a duplicated
snapshot of the filter list and comparing it each time — solely to
support in-place mutation of log_filter_parameters at runtime, a
contract nobody needs. Its two unsynchronized class ivars also allowed
a narrow stale-filter race during reconfiguration.

Invalidation now lives with the data it derives from:

- Config#log_filter_parameters= dups and freezes the assigned list and
  clears the memoized filter; in-place mutation raises FrozenError
  instead of silently depending on the logger noticing
- Config#parameter_filter memoizes ActiveSupport::ParameterFilter until
  reassignment (the constant autoloads via the active_support umbrella
  require in servus.rb, so no explicit require is needed)
- Logger.parameter_filter and its snapshot ivars are removed; log_call
  reads config.parameter_filter directly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rameters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sebscholl
sebscholl merged commit fdad85a into main Aug 15, 2026
4 checks passed
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