Skip to content

Conversation

@jjmata
Copy link
Collaborator

@jjmata jjmata commented Nov 17, 2025

Summary by CodeRabbit

  • Chores
    • Added diagnostic signal handlers to allow on-demand dumping of current application settings for troubleshooting.
    • Installed handlers for both web (Puma worker) and background (Sidekiq) processes to produce consistent diagnostics across processes.
    • Automatically mask sensitive configuration values in diagnostic output to protect secrets.
    • Improved error handling and startup logging around the diagnostic dump to ensure reliable reporting.

Adds signal handling for SIGUSR1 that dumps all application settings
(both declared and dynamic) to Rails.log with sensitive values masked.

- Creates new initializer config/initializers/signal_handlers.rb
- Traps SIGUSR1 signal in both web (Puma) and worker (Sidekiq) processes
- Dumps all declared Setting fields and dynamic Setting keys
- Masks sensitive values (API keys, tokens, secrets, passwords) showing
  only first 4 characters
- Logs to Rails.logger with process information and field counts
- Runs in separate thread to avoid signal handler restrictions
The previous implementation conflicted with Puma's use of SIGUSR1 for
phased restarts, causing the web service to crash with SIGHUP-like
behavior.

Changes:
- Move Puma-specific SIGUSR1 handler to config/puma.rb on_worker_boot
- This sets up the handler in each Puma worker process after Puma's
  initialization, avoiding conflicts with the master process
- Keep Sidekiq handler in initializer (no conflicts there)
- Both web and worker services now properly dump settings on SIGUSR1
- Puma master can still use SIGUSR1 for other purposes if needed

Usage:
- Worker: kill -USR1 <sidekiq_pid>
- Web: kill -USR1 <puma_worker_pid>
@coderabbitai
Copy link

coderabbitai bot commented Nov 17, 2025

Walkthrough

Registers SIGUSR1 handlers for Sidekiq and Puma workers that spawn an asynchronous thread to collect declared Setting methods and Setting.dynamic_keys, mask sensitive values via regex heuristics, and write a structured dump (process info, declared/dynamic sections, counts, and error backtraces) to Rails.logger. (50 words)

Changes

Cohort / File(s) Change Summary
Signal handlers initializer
config/initializers/signal_handlers.rb
New initializer that, after Rails initializes, registers a SIGUSR1 handler for Sidekiq worker processes to trigger an asynchronous settings dump.
Sidekiq runtime wiring
config/initializers/sidekiq.rb
Adds require for lib/settings_log_dump and installs a USR1 trap via SettingsSignalDumper.install_usr1_trap(process_label: "Sidekiq worker") in Sidekiq server configuration.
Puma worker wiring
config/puma.rb
Adds require_relative for the settings dump helper and calls SettingsLogDump.install_usr1_trap(process_label: "Puma worker") in on_worker_boot to install a SIGUSR1 handler per Puma worker.
Settings dump implementation
lib/settings_log_dump.rb
New module SettingsSignalDumper implementing install_usr1_trap and dump_settings: enumerates declared settings, queries Setting.dynamic_keys with a timeout, masks sensitive values using SENSITIVE_PATTERNS, logs structured sections and counts, and rescues/logs errors and backtraces. Exposes SENSITIVE_PATTERNS and SETTINGS_QUERY_TIMEOUT.

Sequence Diagram(s)

sequenceDiagram
    participant Proc as Worker Process (Puma / Sidekiq)
    participant Trap as SIGUSR1 Trap (installed)
    participant Th as Async Thread
    participant S as Setting model
    participant L as Rails.logger

    Proc->>Trap: OS delivers SIGUSR1
    Trap->>Th: spawn asynchronous dump thread
    Th->>L: log header (process label, PID)
    Th->>S: enumerate declared setting methods
    S-->>Th: declared names/values
    Th->>S: fetch dynamic_keys (with timeout)
    S-->>Th: dynamic keys/values or timeout error
    Th->>Th: mask values (SENSITIVE_PATTERNS)
    Th->>L: log Declared Settings section
    Th->>L: log Dynamic Settings section
    Th->>L: log summary and any errors/backtrace
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30–40 minutes

  • Inspect lib/settings_log_dump.rb for correct masking logic, regex coverage, and handling of edge types (nil, booleans, short/long strings).
  • Verify timeout behavior and error handling in dynamic_setting_fields to avoid blocking worker startup or deadlocks.
  • Ensure signal installation in Sidekiq and Puma does not conflict with existing traps or lifecycle hooks and that logging verbosity is acceptable.

Poem

🐰 I heard a soft USR1 call,
I hopped through keys both great and small,
I masked the secrets, counted the rest,
I logged the banners and then took a rest.
🥕 Thump—settings safe and blessed.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add SIGUSR1 trap to dump settings and help debug' clearly and concisely summarizes the main change: adding a SIGUSR1 signal handler to dump application settings for debugging purposes, which aligns directly with the changeset's primary objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/add-sigusr1-settings-dump-015cptPhJDtKzr1qQMZaEGtq

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1fd3e6 and bf173f8.

📒 Files selected for processing (1)
  • config/initializers/sidekiq.rb (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/initializers/sidekiq.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test

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 and usage tips.

@jjmata jjmata self-assigned this Nov 17, 2025
@jjmata jjmata requested a review from sokie November 17, 2025 16:58
@jjmata
Copy link
Collaborator Author

jjmata commented Nov 17, 2025

This should help us debug, want to review @sokie ?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
config/puma.rb (1)

70-78: Consider restricting to public class-level setting accessors

declared_fields currently comes from Setting.singleton_class.instance_methods(false), which may include non-public methods and could raise NoMethodError when used with public_send. To make this more robust, you could use public instance methods on the singleton class instead:

-        declared_fields = Setting.singleton_class.instance_methods(false)
+        declared_fields = Setting.singleton_class.public_instance_methods(false)
           .map(&:to_s)

This keeps the intent (class-level, declared settings) but avoids accidentally including private/protected helpers.

Please confirm via console or a small script which methods are returned by Setting.singleton_class.instance_methods(false) vs public_instance_methods(false) to ensure no desired settings accessors are lost.

Also applies to: 100-118

config/initializers/signal_handlers.rb (1)

19-27: DRY up settings dump logic shared with config/puma.rb

The logic that computes declared_fields, dynamic_fields, and logs their (masked) values is essentially duplicated between this initializer and config/puma.rb. To reduce maintenance overhead and keep masking rules in sync, consider extracting a shared helper, for example:

  • A module method, e.g. SettingsDump.dump!(context_label), or
  • A class method on Setting, e.g. Setting.dump_all(mask_sensitive: true, context: "Sidekiq worker").

Each signal handler would then just call that helper with a context string, keeping the signal wiring local but centralizing the dump/masking behavior.

If you pursue this, please verify both contexts (Puma worker and Sidekiq worker) call the shared helper with appropriate labels and that tests or a manual SIGUSR1 check cover both paths.

Also applies to: 49-71

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0290ed6 and b434322.

📒 Files selected for processing (2)
  • config/initializers/signal_handlers.rb (1 hunks)
  • config/puma.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
config/puma.rb (1)
app/models/setting.rb (1)
  • dynamic_keys (107-109)
config/initializers/signal_handlers.rb (1)
app/models/setting.rb (1)
  • dynamic_keys (107-109)
🪛 GitHub Check: ci / lint
config/puma.rb

[failure] 89-89:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.


[failure] 89-89:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.


[failure] 83-83:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.


[failure] 83-83:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.

config/initializers/signal_handlers.rb

[failure] 38-38:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.


[failure] 38-38:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.


[failure] 32-32:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.


[failure] 32-32:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (4)
config/puma.rb (2)

61-68: SIGUSR1 handler setup for Puma workers looks solid

Using on_worker_boot plus a per-worker Signal.trap("USR1") that offloads work to a new thread is an appropriate pattern and keeps the signal handler non-blocking while providing clear log headers and a completion summary.

Please trigger SIGUSR1 against a Puma worker in a non-production environment to confirm the dump runs to completion without blocking requests and that logs appear once per signal as expected.

Also applies to: 120-128


83-89: Disregard the proposed spacing changes—the current code is already correctly formatted.

The review comment suggests adding spaces inside array brackets, but this would violate RuboCop's default configuration. The Layout/SpaceInsideArrayLiteralBrackets default is EnforcedStyle: no_space, and your .rubocop.yml inherits from rubocop-rails-omakase without overriding this rule. The current code in lines 83–89 (without spaces: [/key/i, ...] and [value.length - 4, 8]) is already compliant and requires no changes.

Likely an incorrect or invalid review comment.

config/initializers/signal_handlers.rb (2)

7-18: Sidekiq-only SIGUSR1 handler wiring is appropriate

Guarding on defined?(Sidekiq) && Sidekiq.server? inside after_initialize cleanly limits this handler to Sidekiq worker processes, and the log line at the end makes it easy to confirm initialization.

Please start a Sidekiq worker in a staging/dev environment, confirm the “Signal handlers initialized…” log appears once on boot, and then send SIGUSR1 to verify the settings dump completes and does not interfere with Sidekiq’s normal signal handling.

Also applies to: 79-80


32-38: The review comment is incorrect—current code already complies with RuboCop defaults.

RuboCop's Layout/SpaceInsideArrayLiteralBrackets rule defaults to EnforcedStyle: no_space, meaning it enforces arrays without spaces. The current code at lines 32 and 38 follows this rule correctly:

  • Line 32: [/key/i, /token/i, ...] ✓ compliant
  • Line 38: [value.length - 4, 8] ✓ compliant

The suggested changes—adding spaces to create [ /key/i, ... ] and [ value.length - 4, 8 ]—would violate the default rule, not satisfy it. Since .rubocop.yml does not explicitly configure this rule, the default no_space style applies.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
config/initializers/signal_handlers.rb (1)

11-77: Identical issues apply here as in puma.rb.

This signal handler has the same concerns already flagged in the review of config/puma.rb:

  1. Database query in signal handler (line 26)
  2. Masking logic doesn't handle empty strings for sensitive fields (line 35)
  3. No safeguard against rapid signal flooding (lines 11-12)
  4. Code duplication (the entire handler logic duplicates puma.rb lines 62-128)

Refer to the comments on config/puma.rb for details and suggested fixes.

🧹 Nitpick comments (3)
config/puma.rb (2)

71-74: Brittle exclusion list for declared fields.

The hardcoded exclusion list %w[[] []= key? delete dynamic_keys validate_onboarding_state! validate_openai_config!] is fragile. If new utility methods are added to the Setting model, they'll appear in the dump unless explicitly excluded. This could leak sensitive data or clutter the output.

Consider documenting this behavior or using a more robust approach, such as marking methods with a custom attribute or checking for a naming convention.


62-63: No safeguard against rapid signal flooding.

If multiple SIGUSR1 signals are sent in quick succession, each will spawn a new thread, potentially creating many concurrent threads that all query the database and write to the logger simultaneously. This could overwhelm resources or produce interleaved, unreadable log output.

Consider adding a simple guard (e.g., a flag or timestamp check) to rate-limit or serialize dump requests.

config/initializers/signal_handlers.rb (1)

79-79: Initialization log may clutter production logs.

Line 79 logs at info level every time a Sidekiq worker starts. In production with frequent worker restarts or autoscaling, this could add noise to the logs. Consider logging at debug level or making it conditional based on environment.

-    Rails.logger.info "Signal handlers initialized for Sidekiq worker (SIGUSR1 -> dump settings)"
+    Rails.logger.debug "Signal handlers initialized for Sidekiq worker (SIGUSR1 -> dump settings)"
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b434322 and ccf34fd.

📒 Files selected for processing (2)
  • config/initializers/signal_handlers.rb (1 hunks)
  • config/puma.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
config/initializers/signal_handlers.rb (1)
app/models/setting.rb (1)
  • dynamic_keys (107-109)
config/puma.rb (1)
app/models/setting.rb (1)
  • dynamic_keys (107-109)

config/puma.rb Outdated
Comment on lines 80 to 98
mask_value = lambda do |field_name, value|
return nil if value.nil?

sensitive = [ /key/i, /token/i, /secret/i, /password/i, /api/i, /credentials?/i, /auth/i ]
is_sensitive = sensitive.any? { |pattern| field_name.match?(pattern) }

if is_sensitive && value.present?
case value
when String
value.length <= 4 ? "[MASKED]" : "#{value[0..3]}#{'*' * [ value.length - 4, 8 ].min}"
when TrueClass, FalseClass
value
else
"[MASKED]"
end
else
value
end
end
Copy link

@coderabbitai coderabbitai bot Nov 17, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Masking logic may not mask empty strings.

Line 86 checks if is_sensitive && value.present?. For sensitive fields with empty string values (""), .present? returns false, so the value would not be masked and would appear unmasked in the logs. While empty strings may not seem sensitive, it's better to consistently mask sensitive field names regardless of content.

Apply this diff to mask empty strings for sensitive fields:

-          if is_sensitive && value.present?
+          if is_sensitive
             case value
             when String
-              value.length <= 4 ? "[MASKED]" : "#{value[0..3]}#{'*' * [ value.length - 4, 8 ].min}"
+              value.empty? ? "[EMPTY]" : (value.length <= 4 ? "[MASKED]" : "#{value[0..3]}#{'*' * [ value.length - 4, 8 ].min}")
             when TrueClass, FalseClass
               value
             else
               "[MASKED]"
             end
           else
             value
           end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mask_value = lambda do |field_name, value|
return nil if value.nil?
sensitive = [ /key/i, /token/i, /secret/i, /password/i, /api/i, /credentials?/i, /auth/i ]
is_sensitive = sensitive.any? { |pattern| field_name.match?(pattern) }
if is_sensitive && value.present?
case value
when String
value.length <= 4 ? "[MASKED]" : "#{value[0..3]}#{'*' * [ value.length - 4, 8 ].min}"
when TrueClass, FalseClass
value
else
"[MASKED]"
end
else
value
end
end
mask_value = lambda do |field_name, value|
return nil if value.nil?
sensitive = [ /key/i, /token/i, /secret/i, /password/i, /api/i, /credentials?/i, /auth/i ]
is_sensitive = sensitive.any? { |pattern| field_name.match?(pattern) }
if is_sensitive
case value
when String
value.empty? ? "[EMPTY]" : (value.length <= 4 ? "[MASKED]" : "#{value[0..3]}#{'*' * [ value.length - 4, 8 ].min}")
when TrueClass, FalseClass
value
else
"[MASKED]"
end
else
value
end
end
🤖 Prompt for AI Agents
config/puma.rb around lines 80 to 98: the current condition `if is_sensitive &&
value.present?` skips masking for empty strings; change the condition to just
`if is_sensitive` (or `if is_sensitive && !value.nil?`) so sensitive field names
are masked regardless of empty-string values, keeping the existing branch
handling (Strings get masked, booleans returned as-is, others "[MASKED]") and
preserving the `return nil if value.nil?` behavior.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Tell me, oh dear @CodeRabbit - how do you mask an empty string? Wouldn't it make things even more confusing by showing ****** when no value was even present?

Copy link

Choose a reason for hiding this comment

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

@jjmata You're absolutely right to question this! My suggestion shows the literal string "[EMPTY]" for empty strings, not asterisks. The idea was to explicitly distinguish between:

  • nil (field not set) → shows as nil
  • "" (field set but empty) → shows as "[EMPTY]"
  • "abc" (short sensitive value) → shows as "[MASKED]"

This way, when debugging, you can tell if a sensitive field like api_key was never configured (nil) versus misconfigured as an empty string ("").

However, you make a valid point - there's no actual sensitive data in an empty string to protect. If you prefer to keep the current behavior where empty strings just appear as empty in the logs (which is clearer and simpler), that's perfectly reasonable. The security concern is minimal since there's nothing to leak.

Your call on what makes the logs most readable for your debugging needs! 🐰

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
lib/settings_log_dump.rb (1)

6-14: Consider more specific pattern for API-related fields.

The /api/i pattern may be overly broad, matching unintended fields like "capital" or "rapid". Consider using a more specific pattern like /\bapi\b/i or /api[_-]?(key|token|secret)/i to reduce false positives.

Apply this diff if you want to be more precise:

     /password/i,
-    /api/i,
+    /\bapi\b/i,
     /credentials?/i,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccf34fd and 6828d83.

📒 Files selected for processing (3)
  • config/initializers/sidekiq.rb (2 hunks)
  • config/puma.rb (2 hunks)
  • lib/settings_log_dump.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/puma.rb
🧰 Additional context used
🧬 Code graph analysis (2)
lib/settings_log_dump.rb (1)
app/models/setting.rb (1)
  • dynamic_keys (107-109)
config/initializers/sidekiq.rb (1)
lib/settings_log_dump.rb (1)
  • install_usr1_trap (18-28)
🪛 GitHub Check: ci / lint
lib/settings_log_dump.rb

[failure] 102-102:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.


[failure] 102-102:
Layout/SpaceInsideArrayLiteralBrackets: Use space inside array brackets.

🔇 Additional comments (4)
lib/settings_log_dump.rb (4)

21-25: Verify thread safety in signal handler context.

Spawning a thread from within a signal handler is generally safer than executing work directly, but be aware that this introduces concurrency into Sidekiq's threading model. Ensure that:

  • The dump_settings method is thread-safe
  • Database connections are available in the spawned thread
  • No deadlocks can occur with Sidekiq's internal locks

Consider testing this in a staging environment under load to verify there are no threading issues.


30-65: Well-structured error handling and logging.

The method includes comprehensive error handling that catches and logs all exceptions with backtrace, ensuring the signal handler won't crash the process. The structured output with headers, sections, and counts makes the dump easy to parse.


67-76: Clean reflection-based field discovery.

The method properly filters out accessors, internal methods, and excluded methods to focus on actual setting fields. Using instance_methods(false) correctly limits to methods defined on the Setting class.


78-87: Database query in signal handler context.

The method queries the database to fetch dynamic keys, which could fail if the database is unavailable or under heavy load. The timeout mitigates hanging, but consider:

  • Database connection pool availability in the spawned thread
  • Impact on database load if multiple processes receive SIGUSR1 simultaneously

The graceful degradation (returning [] on timeout) is good defensive coding.

Verify this behaves correctly when the database is temporarily unavailable or the connection pool is exhausted.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/settings_log_dump.rb (1)

102-102: Fix spacing in array brackets.

The linter flagged spacing inside the array literal brackets.

Apply this diff to fix the spacing:

-          "#{value[0..3]}#{'*' * [ value.length - 4, 8 ].min}"
+          "#{value[0..3]}#{'*' * [value.length - 4, 8].min}"
🧹 Nitpick comments (2)
lib/settings_log_dump.rb (2)

21-25: Consider adding error handling for the spawned thread.

The thread spawned in the signal handler lacks error handling. If dump_settings raises an exception, it may be silently swallowed, making debugging difficult.

Apply this diff to add thread error handling:

     Signal.trap("USR1") do
       Thread.new do
-        dump_settings(process_label: process_label, logger: logger)
+        begin
+          dump_settings(process_label: process_label, logger: logger)
+        rescue => e
+          logger ||= Rails.logger
+          logger.error "Error in USR1 signal handler thread: #{e.class} - #{e.message}"
+          logger.error e.backtrace.join("\n")
+        end
       end
     end

67-76: Consider adding timeout protection for declared settings.

While instance_methods is typically fast, the subsequent public_send calls in dump_settings (line 44) could be slow if any setting accessor performs database queries or external calls.

You could wrap the entire declared settings section in dump_settings with a timeout, though this is likely unnecessary in most cases.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6828d83 and b1fd3e6.

📒 Files selected for processing (1)
  • lib/settings_log_dump.rb (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/settings_log_dump.rb (1)
app/models/setting.rb (1)
  • dynamic_keys (107-109)
🔇 Additional comments (2)
lib/settings_log_dump.rb (2)

1-16: LGTM! Module setup and constants are well-defined.

The sensitive patterns cover common field names, and the timeout value is reasonable for a debug utility.


30-65: LGTM! Well-structured dump logic with appropriate error handling.

The structured output format with clear sections and comprehensive error logging makes this easy to use for debugging.

@we-promise we-promise deleted a comment from coderabbitai bot Nov 17, 2025
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