-
Notifications
You must be signed in to change notification settings - Fork 600
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2177 from newrelic/sidekiq_args_filtration
Sidekiq args filtration
- Loading branch information
Showing
15 changed files
with
653 additions
and
410 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
# This file is distributed under New Relic's license terms. | ||
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. | ||
# frozen_string_literal: true | ||
|
||
module NewRelic | ||
module Agent | ||
module AttributePreFiltering | ||
module_function | ||
|
||
PRE_FILTER_KEYS = %i[include exclude].freeze | ||
DISCARDED = :nr_discarded | ||
|
||
def formulate_regexp_union(option) | ||
return if NewRelic::Agent.config[option].empty? | ||
|
||
Regexp.union(NewRelic::Agent.config[option].map { |p| string_to_regexp(p) }.uniq.compact).freeze | ||
rescue StandardError => e | ||
NewRelic::Agent.logger.warn("Failed to formulate a Regexp union from the '#{option}' configuration option " + | ||
"- #{e.class}: #{e.message}") | ||
end | ||
|
||
def string_to_regexp(str) | ||
Regexp.new(str) | ||
rescue StandardError => e | ||
NewRelic::Agent.logger.warn("Failed to initialize Regexp from string '#{str}' - #{e.class}: #{e.message}") | ||
end | ||
|
||
# attribute filtering suppresses data that has already been flattened | ||
# and coerced (serialized as text) via #flatten_and_coerce, and is | ||
# restricted to basic text matching with a single optional wildcard. | ||
# pre filtering operates on raw Ruby objects beforehand and uses full | ||
# Ruby regex syntax | ||
def pre_filter(values = [], options = {}) | ||
return values unless !options.empty? && PRE_FILTER_KEYS.any? { |k| options.key?(k) } | ||
|
||
# if there's a prefix in play for (non-pre) attribute filtration and | ||
# attribute filtration won't allow that prefix, then don't even bother | ||
# with pre filtration that could only result in values that would be | ||
# blocked | ||
if options.key?(:attribute_namespace) && | ||
!NewRelic::Agent.instance.attribute_filter.might_allow_prefix?(options[:attribute_namespace]) | ||
return values | ||
end | ||
|
||
values.each_with_object([]) do |element, filtered| | ||
object = pre_filter_object(element, options) | ||
filtered << object unless discarded?(object) | ||
end | ||
end | ||
|
||
def pre_filter_object(object, options) | ||
if object.is_a?(Hash) | ||
pre_filter_hash(object, options) | ||
elsif object.is_a?(Array) | ||
pre_filter_array(object, options) | ||
else | ||
pre_filter_scalar(object, options) | ||
end | ||
end | ||
|
||
def pre_filter_hash(hash, options) | ||
filtered_hash = hash.each_with_object({}) do |(key, value), filtered| | ||
filtered_key = pre_filter_object(key, options) | ||
next if discarded?(filtered_key) | ||
|
||
# If the key is permitted, skip include filtration for the value | ||
# but still apply exclude filtration | ||
if options.key?(:exclude) | ||
exclude_only = options.dup | ||
exclude_only.delete(:include) | ||
filtered_value = pre_filter_object(value, exclude_only) | ||
next if discarded?(filtered_value) | ||
else | ||
filtered_value = value | ||
end | ||
|
||
filtered[filtered_key] = filtered_value | ||
end | ||
|
||
filtered_hash.empty? && !hash.empty? ? DISCARDED : filtered_hash | ||
end | ||
|
||
def pre_filter_array(array, options) | ||
filtered_array = array.each_with_object([]) do |element, filtered| | ||
filtered_element = pre_filter_object(element, options) | ||
next if discarded?(filtered_element) | ||
|
||
filtered.push(filtered_element) | ||
end | ||
|
||
filtered_array.empty? && !array.empty? ? DISCARDED : filtered_array | ||
end | ||
|
||
def pre_filter_scalar(scalar, options) | ||
return DISCARDED if options.key?(:include) && !scalar.to_s.match?(options[:include]) | ||
return DISCARDED if options.key?(:exclude) && scalar.to_s.match?(options[:exclude]) | ||
|
||
scalar | ||
end | ||
|
||
# `nil`, empty enumerable objects, and `false` are all valid in their | ||
# own right as application data, so pre-filtering uses a special value | ||
# to indicate that filtered out data has been discarded | ||
def discarded?(object) | ||
object == DISCARDED | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
98 changes: 98 additions & 0 deletions
98
test/multiverse/suites/sidekiq/sidekiq_args_filtration_test.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# This file is distributed under New Relic's license terms. | ||
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. | ||
# frozen_string_literal: true | ||
|
||
require_relative 'sidekiq_test_helpers' | ||
|
||
class SidekiqArgsFiltrationTest < Minitest::Test | ||
include SidekiqTestHelpers | ||
|
||
ARGUMENTS = [{'username' => 'JBond007', | ||
'color' => 'silver', | ||
'record' => true, | ||
'items' => %w[stag thistle peat], | ||
'price_map' => {'apple' => 0.75, 'banana' => 0.50, 'pear' => 0.99}}, | ||
'When I thought I heard myself say no', | ||
false].freeze | ||
|
||
def setup | ||
cache_var = :@nr_attribute_options | ||
if NewRelic::Agent::Instrumentation::Sidekiq::Server.instance_variables.include?(cache_var) | ||
NewRelic::Agent::Instrumentation::Sidekiq::Server.remove_instance_variable(cache_var) | ||
end | ||
end | ||
|
||
def test_by_default_no_args_are_captured | ||
captured_args = run_job_and_get_attributes(*ARGUMENTS) | ||
|
||
assert_empty captured_args, "Didn't expect to capture any attributes for the Sidekiq job, " + | ||
"captured: #{captured_args}" | ||
end | ||
|
||
def test_all_args_are_captured | ||
expected = flatten(ARGUMENTS) | ||
with_config(:'attributes.include' => 'job.sidekiq.args.*') do | ||
captured_args = run_job_and_get_attributes(*ARGUMENTS) | ||
|
||
assert_equal expected, captured_args, "Expected all args to be captured. Wanted:\n\n#{expected}\n\n" + | ||
"Got:\n\n#{captured_args}\n\n" | ||
end | ||
end | ||
|
||
def test_only_included_args_are_captured | ||
included = ['price_map'] | ||
expected = flatten([{included.first => ARGUMENTS.first[included.first]}]) | ||
with_config(:'attributes.include' => 'job.sidekiq.args.*', | ||
:'sidekiq.args.include' => included) do | ||
captured_args = run_job_and_get_attributes(*ARGUMENTS) | ||
|
||
assert_equal expected, captured_args, "Expected only '#{included}' args to be captured. " + | ||
"Wanted:\n\n#{expected}\n\nGot:\n\n#{captured_args}\n\n" | ||
end | ||
end | ||
|
||
def test_excluded_args_are_not_captured | ||
excluded = ['username'] | ||
without_excluded = ARGUMENTS.dup | ||
without_excluded.first.delete(excluded.first) | ||
expected = flatten(without_excluded) | ||
with_config(:'attributes.include' => 'job.sidekiq.args.*', | ||
:'sidekiq.args.exclude' => excluded) do | ||
captured_args = run_job_and_get_attributes(*ARGUMENTS) | ||
|
||
assert_equal expected, captured_args, "Expected '#{excluded}' to be excluded from capture. " + | ||
"Wanted:\n\n#{expected}\n\nGot:\n\n#{captured_args}\n\n" | ||
end | ||
end | ||
|
||
def test_include_and_exclude_cascaded | ||
included = ['price_map'] | ||
excluded = %w[apple pear] | ||
hash = {included.first => ARGUMENTS.first[included.first].dup} | ||
# TODO: OLD RUBIES - Requires 3.0 | ||
# Hash#except would be better here, requires Ruby v3+ | ||
excluded.each { |exclude| hash[included.first].delete(exclude) } | ||
expected = flatten([hash]) | ||
with_config(:'attributes.include' => 'job.sidekiq.args.*', | ||
:'sidekiq.args.include' => included, | ||
:'sidekiq.args.exclude' => excluded) do | ||
captured_args = run_job_and_get_attributes(*ARGUMENTS) | ||
|
||
assert_equal expected, captured_args, "Used included='#{included}', excluded='#{excluded}'. " + | ||
"Wanted:\n\n#{expected}\n\nGot:\n\n#{captured_args}\n\n" | ||
end | ||
end | ||
|
||
def test_arcane_pattern_usage | ||
# no booleans, nothing with numbers, no *.name except unitname, anything ending in 't', a string with I, I, and y, y | ||
excluded = ['^true|false$', '\d+', '(?!<unit)name$', 't$', 'I.*I.*y.*.y'] | ||
expected = flatten([{'color' => 'silver', 'items' => %w[stag thistle]}]) | ||
with_config(:'attributes.include' => 'job.sidekiq.args.*', | ||
:'sidekiq.args.exclude' => excluded) do | ||
captured_args = run_job_and_get_attributes(*ARGUMENTS) | ||
|
||
assert_equal expected, captured_args, "Used excluded='#{excluded}'. " + | ||
"Wanted:\n\n#{expected}\n\nGot:\n\n#{captured_args}\n\n" | ||
end | ||
end | ||
end |
Oops, something went wrong.