Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ 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 strings representing the job argument keys you want to filter out in the UI. This is useful for hiding sensitive user data. Currently, only root-level hash keys are supported.

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
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
23 changes: 23 additions & 0 deletions lib/active_job/job_argument_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 MissionControl::Jobs.filter_arguments.blank?
Comment thread
intrip marked this conversation as resolved.
Outdated

argument.each do |key, value|
if MissionControl::Jobs.filter_arguments.include?(key.to_s)
argument[key] = FILTERED
end
end
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
36 changes: 36 additions & 0 deletions test/active_job/job_argument_filter_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require "test_helper"

class ActiveJob::JobArgumentFilterTest < ActiveSupport::TestCase
test "filter_arguments" do
arguments = [
"deliver",
{
email_address: "jorge@37signals.com",
profile: { name: "Jorge Manrubia" },
message: "Hello!"
}
]
@previous_filter_arguments, MissionControl::Jobs.filter_arguments = MissionControl::Jobs.filter_arguments, %w[ 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])
ensure
MissionControl::Jobs.filter_arguments = @previous_filter_arguments
end

test "filter_argument_hash" do
argument = {
email_address: "jorge@37signals.com",
message: "Hello!"
}
@previous_filter_arguments, MissionControl::Jobs.filter_arguments = MissionControl::Jobs.filter_arguments, %w[ message ]

filtered = ActiveJob::JobArgumentFilter.filter_argument_hash(argument)

assert_equal({ email_address: "jorge@37signals.com", message: "[FILTERED]" }, filtered)
ensure
MissionControl::Jobs.filter_arguments = @previous_filter_arguments
end
end
3 changes: 3 additions & 0 deletions test/dummy/config/initializers/mission_control_jobs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ def redis_connection_for(app, server)
Resque::DataStore.new redis_namespace
end

# Filter sensitive arguments from the UI.
MissionControl::Jobs.filter_arguments = %w[ author ]

SERVERS_BY_APP.each do |app, servers|
queue_adapters_by_name = servers.collect do |server|
queue_adapter = if server.start_with?("resque")
Expand Down
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, %w[ 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