PageRenderTime 34ms CodeModel.GetById 7ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/new_relic/agent/attribute_filter.rb

http://github.com/newrelic/rpm
Ruby | 301 lines | 174 code | 44 blank | 83 comment | 28 complexity | d555bf5a2a9ace5083fe273d90a3debb MD5 | raw file
Possible License(s): Apache-2.0
  1. # encoding: utf-8
  2. # This file is distributed under New Relic's license terms.
  3. # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
  4. # This class applies filtering rules as specified in the Agent Attributes
  5. # cross-agent spec.
  6. #
  7. # Instances of it are constructed by deriving a set of rules from the agent
  8. # configuration. Instances are immutable once they are constructed - if the
  9. # config changes, a new instance should be constructed and swapped in to
  10. # replace the existing one.
  11. #
  12. # The #apply method is the main external interface of this class. It takes an
  13. # attribute name and a set of default destinations (represented as a bitfield)
  14. # and returns a set of actual destinations after applying the filtering rules
  15. # specified in the config.
  16. #
  17. # Each set of destinations is represented as a bitfield, where the bit positions
  18. # specified in the DST_* constants are used to indicate whether an attribute
  19. # should be sent to the corresponding destination.
  20. #
  21. # The choice of a bitfield here rather than an Array was made to avoid the need
  22. # for any transient object allocations during the application of rules. Since
  23. # rule application will happen once per attribute per transaction, this is a hot
  24. # code path.
  25. #
  26. # The algorithm for applying filtering rules is as follows:
  27. #
  28. # 1. Start with a bitfield representing the set of default destinations passed
  29. # in to #apply.
  30. # 2. Mask this bitfield against the set of destinations that have attribute
  31. # enabled at all.
  32. # 3. Traverse the list of rules in order (more on the ordering later), applying
  33. # each matching rule, but taking care to not let rules override the enabled
  34. # status of each destination. Each matching rule may mutate the bitfield.
  35. # 4. Return the resulting bitfield after all rules have been applied.
  36. #
  37. # Each rule consists of a name, a flag indicating whether it ends with a
  38. # wildcard, a bitfield representing the set of destinations that it applies to,
  39. # and a flag specifying whether it is an include or exclude rule.
  40. #
  41. # During construction, rules are sorted according to the following criteria:
  42. #
  43. # 1. First, the names are compared lexicographically. This has the impact of
  44. # forcing shorter (more general) rules towards the top of the list and longer
  45. # (more specific) rules towards the bottom. This is important, because the
  46. # Agent Attributes spec stipulates that the most specific rule for a given
  47. # destination should take precedence. Since rules are applied top-to-bottom,
  48. # this sorting guarantees that the most specific rule will be applied last.
  49. # 2. If the names are identical, we next examine the wildcard flag. Rules ending
  50. # with a wildcard are considered more general (and thus 'less than') rules
  51. # not ending with a wildcard.
  52. # 3. If the names and wildcard flags are identical, we next examine whether the
  53. # rules being compared are include or exclude rules. Exclude rules have
  54. # precedence by the spec, so they are considered 'greater than' include
  55. # rules.
  56. #
  57. # This approach to rule evaluation was taken from the PHP agent's
  58. # implementation.
  59. #
  60. module NewRelic
  61. module Agent
  62. class AttributeFilter
  63. DST_NONE = 0x0
  64. DST_TRANSACTION_EVENTS = 1 << 0
  65. DST_TRANSACTION_TRACER = 1 << 1
  66. DST_ERROR_COLLECTOR = 1 << 2
  67. DST_BROWSER_MONITORING = 1 << 3
  68. DST_SPAN_EVENTS = 1 << 4
  69. DST_TRANSACTION_SEGMENTS = 1 << 5
  70. DST_ALL = 0x3f
  71. attr_reader :rules
  72. def initialize(config)
  73. @enabled_destinations = DST_NONE
  74. @enabled_destinations |= DST_TRANSACTION_TRACER if config[:'transaction_tracer.attributes.enabled']
  75. @enabled_destinations |= DST_TRANSACTION_EVENTS if config[:'transaction_events.attributes.enabled']
  76. @enabled_destinations |= DST_ERROR_COLLECTOR if config[:'error_collector.attributes.enabled']
  77. @enabled_destinations |= DST_BROWSER_MONITORING if config[:'browser_monitoring.attributes.enabled']
  78. @enabled_destinations |= DST_SPAN_EVENTS if config[:'span_events.attributes.enabled']
  79. @enabled_destinations |= DST_TRANSACTION_SEGMENTS if config[:'transaction_segments.attributes.enabled']
  80. @enabled_destinations = DST_NONE unless config[:'attributes.enabled']
  81. @rules = []
  82. build_rule(config[:'attributes.exclude'], DST_ALL, false)
  83. build_rule(config[:'transaction_tracer.attributes.exclude'], DST_TRANSACTION_TRACER, false)
  84. build_rule(config[:'transaction_events.attributes.exclude'], DST_TRANSACTION_EVENTS, false)
  85. build_rule(config[:'error_collector.attributes.exclude'], DST_ERROR_COLLECTOR, false)
  86. build_rule(config[:'browser_monitoring.attributes.exclude'], DST_BROWSER_MONITORING, false)
  87. build_rule(config[:'span_events.attributes.exclude'], DST_SPAN_EVENTS, false)
  88. build_rule(config[:'transaction_segments.attributes.exclude'], DST_TRANSACTION_SEGMENTS, false)
  89. build_rule(['request.parameters.*'], include_destinations_for_capture_params(config[:capture_params]), true)
  90. build_rule(['job.resque.args.*'], include_destinations_for_capture_params(config[:'resque.capture_params']), true)
  91. build_rule(['job.sidekiq.args.*'], include_destinations_for_capture_params(config[:'sidekiq.capture_params']), true)
  92. build_rule(['host', 'port_path_or_id'], DST_TRANSACTION_SEGMENTS, config[:'datastore_tracer.instance_reporting.enabled'])
  93. build_rule(['database_name'], DST_TRANSACTION_SEGMENTS, config[:'datastore_tracer.database_name_reporting.enabled'])
  94. build_rule(config[:'attributes.include'], DST_ALL, true)
  95. build_rule(config[:'transaction_tracer.attributes.include'], DST_TRANSACTION_TRACER, true)
  96. build_rule(config[:'transaction_events.attributes.include'], DST_TRANSACTION_EVENTS, true)
  97. build_rule(config[:'error_collector.attributes.include'], DST_ERROR_COLLECTOR, true)
  98. build_rule(config[:'browser_monitoring.attributes.include'], DST_BROWSER_MONITORING, true)
  99. build_rule(config[:'span_events.attributes.include'], DST_SPAN_EVENTS, true)
  100. build_rule(config[:'transaction_segments.attributes.include'], DST_TRANSACTION_SEGMENTS, true)
  101. build_uri_rule(config[:'attributes.exclude'])
  102. @rules.sort!
  103. # We're ok to cache high security for fast lookup because the attribute
  104. # filter is re-generated on any significant config change.
  105. @high_security = config[:high_security]
  106. setup_key_cache
  107. cache_prefix_denylist
  108. end
  109. # Note the key_cache is a global cache, accessible by multiple threads,
  110. # but is intentionally left unsynchronized for liveness. Writes will always
  111. # involve writing the same boolean value for each key, so there is no
  112. # worry of one value clobbering another. For reads, if a value hasn't been
  113. # written to the cache yet, the worst that will happen is that it will run
  114. # through the filter rules again. Both reads and writes will become
  115. # eventually consistent.
  116. def setup_key_cache
  117. destinations = [
  118. DST_TRANSACTION_EVENTS,
  119. DST_TRANSACTION_TRACER,
  120. DST_ERROR_COLLECTOR,
  121. DST_BROWSER_MONITORING,
  122. DST_SPAN_EVENTS,
  123. DST_TRANSACTION_SEGMENTS,
  124. DST_ALL
  125. ]
  126. @key_cache = destinations.inject({}) do |memo, destination|
  127. memo[destination] = {}
  128. memo
  129. end
  130. end
  131. def include_destinations_for_capture_params(capturing)
  132. if capturing
  133. DST_TRANSACTION_TRACER | DST_ERROR_COLLECTOR
  134. else
  135. DST_NONE
  136. end
  137. end
  138. def build_rule(attribute_names, destinations, is_include)
  139. attribute_names.each do |attribute_name|
  140. rule = AttributeFilterRule.new(attribute_name, destinations, is_include)
  141. @rules << rule unless rule.empty?
  142. end
  143. end
  144. def build_uri_rule(excluded_attributes)
  145. uri_aliases = %w(uri url request_uri request.uri http.url)
  146. if (excluded_attributes & uri_aliases).size > 0
  147. build_rule(uri_aliases - excluded_attributes, DST_ALL, false)
  148. end
  149. end
  150. def apply(attribute_name, default_destinations)
  151. return DST_NONE if @enabled_destinations == DST_NONE
  152. destinations = default_destinations
  153. attribute_name = attribute_name.to_s
  154. @rules.each do |rule|
  155. if rule.match?(attribute_name)
  156. if rule.is_include
  157. destinations |= rule.destinations
  158. else
  159. destinations &= rule.destinations
  160. end
  161. end
  162. end
  163. destinations & @enabled_destinations
  164. end
  165. def allows?(allowed_destinations, requested_destination)
  166. allowed_destinations & requested_destination == requested_destination
  167. end
  168. def allows_key?(key, destination)
  169. return false unless destination & @enabled_destinations == destination
  170. value = @key_cache[destination][key]
  171. if value.nil?
  172. allowed_destinations = apply(key, destination)
  173. @key_cache[destination][key] = allows?(allowed_destinations, destination)
  174. else
  175. value
  176. end
  177. end
  178. def high_security?
  179. @high_security
  180. end
  181. # For attribute prefixes where we know the default destinations will
  182. # always be DST_NONE, we can statically determine that any attribute
  183. # starting with the prefix will not be allowed unless there's an include
  184. # rule that might match attributes starting with it.
  185. #
  186. # This allows us to skip significant preprocessing work (hash/array
  187. # flattening and type coercion) for HTTP request parameters and job
  188. # arguments for Sidekiq and Resque in the common case, since none of
  189. # these attributes are captured by default.
  190. #
  191. def cache_prefix_denylist
  192. @prefix_denylist = {}
  193. @prefix_denylist[:'request.parameters'] = true unless might_allow_prefix_uncached?(:'request.parameters')
  194. @prefix_denylist[:'job.sidekiq.args'] = true unless might_allow_prefix_uncached?(:'job.sidekiq.args')
  195. @prefix_denylist[:'job.resque.args'] = true unless might_allow_prefix_uncached?(:'job.resque.args')
  196. end
  197. # Note that the given prefix *must* be a Symbol
  198. def might_allow_prefix?(prefix)
  199. !@prefix_denylist.include?(prefix)
  200. end
  201. def might_allow_prefix_uncached?(prefix)
  202. prefix = prefix.to_s
  203. @rules.any? do |rule|
  204. if rule.is_include
  205. if rule.wildcard
  206. if rule.attribute_name.size > prefix.size
  207. rule.attribute_name.start_with?(prefix)
  208. else
  209. prefix.start_with?(rule.attribute_name)
  210. end
  211. else
  212. rule.attribute_name.start_with?(prefix)
  213. end
  214. end
  215. end
  216. end
  217. end
  218. class AttributeFilterRule
  219. attr_reader :attribute_name, :destinations, :is_include, :wildcard
  220. def initialize(attribute_name, destinations, is_include)
  221. @attribute_name = attribute_name.sub(/\*$/, "")
  222. @wildcard = attribute_name.end_with?("*")
  223. @is_include = is_include
  224. @destinations = is_include ? destinations : ~destinations
  225. end
  226. # Rules are sorted from least specific to most specific
  227. #
  228. # All else being the same, wildcards are considered less specific
  229. # All else being the same, include rules are less specific than excludes
  230. def <=>(other)
  231. name_cmp = @attribute_name <=> other.attribute_name
  232. return name_cmp unless name_cmp == 0
  233. if wildcard != other.wildcard
  234. return wildcard ? -1 : 1
  235. end
  236. if is_include != other.is_include
  237. return is_include ? -1 : 1
  238. end
  239. return 0
  240. end
  241. def match?(name)
  242. if wildcard
  243. name.start_with?(@attribute_name)
  244. else
  245. @attribute_name == name
  246. end
  247. end
  248. def empty?
  249. if is_include
  250. @destinations == AttributeFilter::DST_NONE
  251. else
  252. @destinations == AttributeFilter::DST_ALL
  253. end
  254. end
  255. end
  256. end
  257. end