Skip to content

Filtering Job Arguments #277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ Besides `base_controller_class`, you can also set the following for `MissionCont
- `scheduled_job_delay_threshold`: the time duration before a scheduled job is considered delayed. Defaults to `1.minute` (a job is considered delayed if it hasn't transitioned from the `scheduled` status 1 minute after the scheduled time).
- `show_console_help`: whether to show the console help. If you don't want the console help message, set this to `false`—defaults to `true`.
- `backtrace_cleaner`: a backtrace cleaner used for optionally filtering backtraces on the Failed Jobs detail page. Defaults to `Rails::BacktraceCleaner.new`. See the [Advanced configuration](#advanced-configuration) section for how to configure/override this setting on a per application/server basis.
- `filter_arguments`: an array of job argument keys that you want to filter out in the UI. This is useful for hiding sensitive user data. You can also override this option for each application. Currently, only root-level hash keys are supported, and it only works with the `Resque` and `SolidQueue` adapters. See the [Advanced configuration](#advanced-configuration) section for an example.
```ruby

This library extends Active Job with a querying interface and the following setting:
- `config.active_job.default_page_size`: the internal batch size that Active Job will use when sending queries to the underlying adapter and the batch size for the bulk operations defined above—defaults to `1000`.
Expand Down Expand Up @@ -184,7 +186,9 @@ SERVERS_BY_APP.each do |app, servers|
# [ server, [ queue_adapter, BacktraceCleaner.new ]] # with optional backtrace cleaner
end.to_h

MissionControl::Jobs.applications.add(app, queue_adapters_by_name)
filter_arguments = %i[ author ]

MissionControl::Jobs.applications.add(app, queue_adapters_by_name, filter_arguments)
end
```

Expand Down
3 changes: 2 additions & 1 deletion app/helpers/mission_control/jobs/jobs_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def as_renderable_hash(argument)
elsif argument["_aj_serialized"]
ActiveJob::Arguments.deserialize([ argument ]).first
else
argument.without("_aj_symbol_keys", "_aj_ruby2_keywords")
ActiveJob::JobArgumentFilter.filter_argument_hash(argument)
.without("_aj_symbol_keys", "_aj_ruby2_keywords")
.transform_values { |v| as_renderable_argument(v) }
.map { |k, v| "#{k}: #{v}" }
.join(", ")
Expand Down
28 changes: 28 additions & 0 deletions lib/active_job/job_argument_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class ActiveJob::JobArgumentFilter
FILTERED = "[FILTERED]"

class << self
def filter_arguments(arguments)
arguments.each do |argument|
if argument.is_a?(Hash)
filter_argument_hash(argument)
end
end
end

def filter_argument_hash(argument)
return argument if filters.blank?

argument.each do |key, value|
if filters.include?(key.to_s)
argument[key] = FILTERED
end
end
end

private
def filters
MissionControl::Jobs::Current.application&.filter_arguments
end
end
end
2 changes: 2 additions & 0 deletions lib/active_job/queue_adapters/resque_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ def fetch_queue_resque_jobs

def deserialize_resque_job(resque_job_hash, index)
args_hash = resque_job_hash.dig("payload", "args") || resque_job_hash.dig("args")
ActiveJob::JobArgumentFilter.filter_arguments(args_hash) if args_hash

ActiveJob::JobProxy.new(args_hash&.first).tap do |job|
job.last_execution_error = execution_error_from_resque_job(resque_job_hash)
job.raw_data = resque_job_hash
Expand Down
8 changes: 7 additions & 1 deletion lib/active_job/queue_adapters/solid_queue_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
ActiveJob::JobProxy.new(solid_queue_job.arguments).tap do |job|
job.status = job_status
job.last_execution_error = execution_error_from_solid_queue_job(solid_queue_job) if job_status == :failed
job.raw_data = solid_queue_job.as_json
job.raw_data = filter_raw_data_arguments(solid_queue_job.as_json)
job.failed_at = solid_queue_job&.failed_execution&.created_at if job_status == :failed
job.finished_at = solid_queue_job.finished_at
job.blocked_by = solid_queue_job.concurrency_key
Expand All @@ -109,6 +109,12 @@ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
end
end

def filter_raw_data_arguments(raw_data)
arguments = raw_data.dig("arguments", "arguments")
ActiveJob::JobArgumentFilter.filter_arguments(arguments)
raw_data
end

def status_from_solid_queue_job(solid_queue_job)
SolidQueueJobs::STATUS_MAP.invert[solid_queue_job.status]
end
Expand Down
2 changes: 2 additions & 0 deletions lib/mission_control/jobs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ module Jobs
mattr_accessor :show_console_help, default: true
mattr_accessor :backtrace_cleaner

mattr_accessor :filter_arguments, default: []

mattr_accessor :importmap, default: Importmap::Map.new

mattr_accessor :http_basic_auth_user
Expand Down
7 changes: 6 additions & 1 deletion lib/mission_control/jobs/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
class MissionControl::Jobs::Application
include MissionControl::Jobs::IdentifiedByName

attr_reader :servers
attr_reader :servers, :filter_arguments

def initialize(name:)
super
@servers = MissionControl::Jobs::IdentifiedElements.new
@filter_arguments = []
end

def add_servers(queue_adapters_by_name)
Expand All @@ -17,4 +18,8 @@ def add_servers(queue_adapters_by_name)
backtrace_cleaner: cleaner, application: self)
end
end

def filter_arguments=(arguments)
@filter_arguments = Array(arguments).map(&:to_s)
end
end
3 changes: 2 additions & 1 deletion lib/mission_control/jobs/applications.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# A container to register applications
class MissionControl::Jobs::Applications < MissionControl::Jobs::IdentifiedElements
def add(name, queue_adapters_by_name = {})
def add(name, queue_adapters_by_name = {}, filter_arguments = [])
self << MissionControl::Jobs::Application.new(name: name).tap do |application|
application.add_servers(queue_adapters_by_name)
application.filter_arguments = filter_arguments.presence || MissionControl::Jobs.filter_arguments
end
end
end
38 changes: 38 additions & 0 deletions test/active_job/job_argument_filter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require "test_helper"

class ActiveJob::JobArgumentFilterTest < ActiveSupport::TestCase
setup do
@application = MissionControl::Jobs::Application.new(name: "BC4")
MissionControl::Jobs::Current.application = @application
end

test "filter_arguments" do
arguments = [
"deliver",
{
email_address: "[email protected]",
profile: { name: "Jorge Manrubia" },
message: "Hello!"
}
]
@application.filter_arguments = %i[ email_address message ]

filtered = ActiveJob::JobArgumentFilter.filter_arguments(arguments)

assert_equal "deliver", filtered[0]
assert_equal({ email_address: "[FILTERED]", profile: { name: "Jorge Manrubia" }, message: "[FILTERED]" }, filtered[1])
end

test "filter_argument_hash" do
argument = {
email_address: "[email protected]",
message: "Hello!"
}
filtered = ActiveJob::JobArgumentFilter.filter_argument_hash(argument)
assert_equal({ email_address: "[email protected]", message: "Hello!" }, filtered)

@application.filter_arguments = %i[ message ]
filtered = ActiveJob::JobArgumentFilter.filter_argument_hash(argument)
assert_equal({ email_address: "[email protected]", message: "[FILTERED]" }, filtered)
end
end
4 changes: 3 additions & 1 deletion test/dummy/config/initializers/mission_control_jobs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ def redis_connection_for(app, server)
[ server, queue_adapter ]
end.to_h

MissionControl::Jobs.applications.add(app, queue_adapters_by_name)
filter_arguments = %i[ author ]

MissionControl::Jobs.applications.add(app, queue_adapters_by_name, filter_arguments)
end
2 changes: 1 addition & 1 deletion test/dummy/db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def load_finished_jobs

def load_failed_jobs
puts "Generating #{failed_jobs_count} failed jobs for #{application} - #{server}..."
failed_jobs_count.times { |index| enqueue_one_of FailingJob => index, FailingReloadedJob => index, FailingPostJob => [ Post.last, 1.year.ago ] }
failed_jobs_count.times { |index| enqueue_one_of FailingJob => index, FailingReloadedJob => index, FailingPostJob => [ Post.last, 1.year.ago, author: "Jorge" ] }
perform_jobs
end

Expand Down
16 changes: 16 additions & 0 deletions test/system/show_failed_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ class ShowFailedJobsTest < ApplicationSystemTestCase
assert_text /failing_job.rb/
end

test "filtered arguments are hidden" do
ActiveJob.jobs.failed.discard_all
FailingPostJob.perform_later(Post.create(title: "hello_world"), 1.year.ago, author: "Jorge")
perform_enqueued_jobs
@previous_filter_arguments, MissionControl::Jobs.filter_arguments = MissionControl::Jobs.filter_arguments, %i[ author ]

visit jobs_path(:failed)
click_on "FailingPostJob"

assert_text /dummy\/post/i
assert_text /\[FILTERED\]/
assert_no_text /Jorge/
ensure
MissionControl::Jobs.filter_arguments = @previous_filter_arguments
end

test "click on a failed job error to see its error information" do
within_job_row /FailingJob\s*2/ do
click_on "RuntimeError: This always fails!"
Expand Down