scout_apm 2.6.3 → 2.6.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +32 -0
  3. data/Gemfile +4 -0
  4. data/gems/rails3.gemfile +1 -0
  5. data/lib/scout_apm.rb +2 -0
  6. data/lib/scout_apm/auto_instrument/instruction_sequence.rb +1 -1
  7. data/lib/scout_apm/background_job_integrations/legacy_sneakers.rb +55 -0
  8. data/lib/scout_apm/background_job_integrations/sidekiq.rb +2 -2
  9. data/lib/scout_apm/background_job_integrations/sneakers.rb +11 -11
  10. data/lib/scout_apm/detailed_trace.rb +2 -1
  11. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  12. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +1 -0
  13. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +32 -15
  14. data/lib/scout_apm/instruments/action_view.rb +7 -2
  15. data/lib/scout_apm/instruments/net_http.rb +8 -1
  16. data/lib/scout_apm/job_record.rb +4 -2
  17. data/lib/scout_apm/layaway_file.rb +4 -0
  18. data/lib/scout_apm/layer_children_set.rb +9 -8
  19. data/lib/scout_apm/layer_converters/trace_converter.rb +3 -0
  20. data/lib/scout_apm/remote/message.rb +4 -0
  21. data/lib/scout_apm/serializers/app_server_load_serializer.rb +4 -0
  22. data/lib/scout_apm/serializers/directive_serializer.rb +4 -0
  23. data/lib/scout_apm/utils/marshal_logging.rb +90 -0
  24. data/lib/scout_apm/utils/sql_sanitizer.rb +9 -1
  25. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +7 -0
  26. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +5 -0
  27. data/lib/scout_apm/version.rb +1 -1
  28. data/test/test_helper.rb +1 -1
  29. data/test/unit/layer_children_set_test.rb +9 -0
  30. data/test/unit/sql_sanitizer_test.rb +40 -0
  31. metadata +6 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a0344ecd846204d2b4057246c9719ebf4453a1c240847e54357a75cecda39b6
4
- data.tar.gz: 55b8b4c379fee7e5f8275bde73f27fa7d9e1e16b560c70207e51e4225e45184c
3
+ metadata.gz: d99ef266b1387f4a51d49467a3d6cad6499f3054834d8695b5e0f3af1d7a36b3
4
+ data.tar.gz: 04f505997a14d3312de48905b27aab38beedf846de0e7c9411d4e71e1b4477c4
5
5
  SHA512:
6
- metadata.gz: 45cd0a3dc5b6632a980a38324e6c933d2552eb33aa32e1e9e9da74e5a6b848b5bdb694c27f017502c6a437dd6576486d8186ccbea8ae3c5563b8778bac158870
7
- data.tar.gz: d20753a142c71ef03a47608bcae15acdb2aeec11e5f7f75c78ece78657ac828b077af15c152b4e1aac92eb81f98e6a7b211db1aa35349cd8746a37ae76089b25
6
+ metadata.gz: 5918121a80a2dd6e15c12f2c45ae294c8f2dbb815296b1d63f9dbd5065e97e10312c4c7674a3c89496dc91303c47aa8f2d61b19005f130f40ceceba6ebd170da
7
+ data.tar.gz: 2033f3db42f9f0a3626c540e3beecb68aaf3480a4e88bdbaae43a7fe0a4273c06ad4b15c546b8a4d4051e7b8d96526e80847129dba9a9ed66fd4efa0b27cadc4
@@ -1,3 +1,34 @@
1
+ # 2.6.8
2
+
3
+ * Lock rake version for 1.8.7 to older version (#329)
4
+ * Delete unneeded .DS_Store file that snuck in (#334)
5
+ * Fix typo in "queue_time_ms"
6
+ * Fix Rails 6 deprecation warning at boot time (#337)
7
+ * Fix partial naming on Rails 6.0 (#339)
8
+ * Support Sidekiq 6.1 instrumentation (#340)
9
+
10
+ # 2.6.7
11
+
12
+ * Remove accidental call to `as_json`
13
+
14
+ # 2.6.6
15
+
16
+ * Add basic support for parsing Microsoft SQLServer queries (#317)
17
+ * Refine Postgresql Sanitization with subqueries and JSON operations (#262)
18
+
19
+ # 2.6.5
20
+
21
+ * Add a tag to any requests that reach maximum number of spans (#316)
22
+ * Update testing library Mocha (#315)
23
+ * Fix case sensitivity mismatch in Job renaming (#314)
24
+ * Add support for Sneakers 2.5 (#313)
25
+ * Fix edge case with Resque instrumentation (#312)
26
+ * Fix missing source code when used with BugSnag (#308)
27
+
28
+ # 2.6.4
29
+
30
+ * Add defensive check against a nil @address in Net/HTTP instruments (#306)
31
+
1
32
  # 2.6.3
2
33
 
3
34
  * Standardize Metadata with other language agents (#302)
@@ -22,6 +53,7 @@
22
53
  # 2.5.3
23
54
 
24
55
  * Add Que support (#265)
56
+ * Add Memcached support (#279)
25
57
 
26
58
  # 2.5.2
27
59
 
data/Gemfile CHANGED
@@ -10,4 +10,8 @@ if RUBY_VERSION <= "1.8.7"
10
10
  gem "pry", "~> 0.9.12"
11
11
  gem "rake", "~> 10.5"
12
12
  gem "minitest", "~> 5.11.3"
13
+ elsif RUBY_VERSION <= "1.9.3"
14
+ gem "rake", "~> 10.5"
15
+ else
16
+ gem "rake", ">= 12.3.3"
13
17
  end
@@ -1,4 +1,5 @@
1
1
  eval_gemfile("../Gemfile")
2
2
 
3
+ gem "json", "1.8.6"
3
4
  gem "rails", "~> 3.2"
4
5
  gem "sqlite3", "~> 1.3.5"
@@ -63,6 +63,7 @@ require 'scout_apm/background_job_integrations/resque'
63
63
  require 'scout_apm/background_job_integrations/shoryuken'
64
64
  require 'scout_apm/background_job_integrations/sneakers'
65
65
  require 'scout_apm/background_job_integrations/que'
66
+ require 'scout_apm/background_job_integrations/legacy_sneakers'
66
67
 
67
68
  require 'scout_apm/framework_integrations/rails_2'
68
69
  require 'scout_apm/framework_integrations/rails_3_or_4'
@@ -112,6 +113,7 @@ require 'scout_apm/utils/time'
112
113
  require 'scout_apm/utils/unique_id'
113
114
  require 'scout_apm/utils/numbers'
114
115
  require 'scout_apm/utils/gzip_helper'
116
+ require 'scout_apm/utils/marshal_logging'
115
117
 
116
118
  require 'scout_apm/config'
117
119
  require 'scout_apm/environment'
@@ -8,7 +8,7 @@ module ScoutApm
8
8
  if Rails.controller_path?(path) & !Rails.ignore?(path)
9
9
  begin
10
10
  new_code = Rails.rewrite(path)
11
- return self.compile(new_code, File.basename(path), path)
11
+ return self.compile(new_code, path, path)
12
12
  rescue
13
13
  warn "Failed to apply auto-instrumentation to #{path}: #{$!}"
14
14
  end
@@ -0,0 +1,55 @@
1
+ # This is different than other BackgroundJobIntegrations and must be prepended
2
+ # manually in each job.
3
+ #
4
+ # class MyWorker
5
+ # prepend ScoutApm::BackgroundJobIntegrations::LegacySneakers
6
+ #
7
+ # def work(msg)
8
+ # ...
9
+ # end
10
+ # end
11
+ module ScoutApm
12
+ module BackgroundJobIntegrations
13
+ module LegacySneakers
14
+ UNKNOWN_QUEUE_PLACEHOLDER = 'default'.freeze
15
+
16
+ def self.prepended(base)
17
+ ScoutApm::Agent.instance.logger.info("Prepended LegacySneakers in #{base}")
18
+ end
19
+
20
+ def initialize(*args)
21
+ super
22
+
23
+ # Save off the existing value to call the correct existing work
24
+ # function in the instrumentation. But then override Sneakers to always
25
+ # use the extra-argument version, which has data Scout needs
26
+ @call_work = respond_to?(:work)
27
+ end
28
+
29
+ def work_with_params(msg, delivery_info, metadata)
30
+ queue = delivery_info[:routing_key] || UNKNOWN_QUEUE_PLACEHOLDER
31
+ job_class = self.class.name
32
+ req = ScoutApm::RequestManager.lookup
33
+
34
+ begin
35
+ req.start_layer(ScoutApm::Layer.new('Queue', queue))
36
+ started_queue = true
37
+ req.start_layer(ScoutApm::Layer.new('Job', job_class))
38
+ started_job = true
39
+
40
+ if @call_work
41
+ work(msg)
42
+ else
43
+ super
44
+ end
45
+ rescue Exception
46
+ req.error!
47
+ raise
48
+ ensure
49
+ req.stop_layer if started_job
50
+ req.stop_layer if started_queue
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -40,10 +40,10 @@ module ScoutApm
40
40
  require 'sidekiq/processor' # sidekiq v4 has not loaded this file by this point
41
41
 
42
42
  ::Sidekiq::Processor.class_eval do
43
- def initialize_with_scout(boss)
43
+ def initialize_with_scout(*args)
44
44
  agent = ::ScoutApm::Agent.instance
45
45
  agent.start
46
- initialize_without_scout(boss)
46
+ initialize_without_scout(*args)
47
47
  end
48
48
 
49
49
  alias_method :initialize_without_scout, :initialize
@@ -1,14 +1,22 @@
1
1
  module ScoutApm
2
2
  module BackgroundJobIntegrations
3
3
  class Sneakers
4
- attr_reader :logger
5
-
6
4
  def name
7
5
  :sneakers
8
6
  end
9
7
 
10
8
  def present?
11
- defined?(::Sneakers)
9
+ defined?(::Sneakers) && supported_version?
10
+ end
11
+
12
+ # Only support Sneakers 2.7 and up
13
+ def supported_version?
14
+ result = Gem::Version.new(::Sneakers::VERSION) > Gem::Version.new("2.7.0")
15
+ ScoutApm::Agent.instance.logger.info("Skipping Sneakers instrumentation. Only versions 2.7+ are supported. See docs or contact support@scoutapm.com for instrumentation of older versions.")
16
+ result
17
+ rescue
18
+ ScoutApm::Agent.instance.logger.info("Failed comparing Sneakers Version. Skipping")
19
+ false
12
20
  end
13
21
 
14
22
  def forking?
@@ -69,14 +77,6 @@ module ScoutApm
69
77
  alias_method :process_work_without_scout, :process_work
70
78
  alias_method :process_work, :process_work_with_scout
71
79
  end
72
-
73
- # msg = {
74
- # "job_class":"DummyWorker",
75
- # "job_id":"ea23ba1c-3022-4e05-870b-c3bcb1c4f328",
76
- # "queue_name":"default",
77
- # "arguments":["fjdkl"],
78
- # "locale":"en"
79
- # }
80
80
  end
81
81
 
82
82
  ACTIVE_JOB_KLASS = 'ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper'.freeze
@@ -200,8 +200,9 @@ class DetailedTraceTags
200
200
  @tags = hash
201
201
  end
202
202
 
203
+ # @tags is already a hash, so no conversion needed
203
204
  def as_json(*)
204
- @tags.as_json
205
+ @tags
205
206
  end
206
207
  end
207
208
 
@@ -26,7 +26,7 @@ module ScoutApm
26
26
  # The time in queue of the transaction in ms. If not present, +nil+ is returned as this is unknown.
27
27
  def queue_time_ms
28
28
  # Controller logic
29
- if converter_results[:queue_time] && converter_results[:queue].any?
29
+ if converter_results[:queue_time] && converter_results[:queue_time].any?
30
30
  converter_results[:queue_time].values.first.total_call_time*1000 # ms
31
31
  # Job logic
32
32
  elsif converter_results[:job]
@@ -48,6 +48,7 @@ module ScoutApm
48
48
  when "sqlite" then :sqlite
49
49
  when "mysql" then :mysql
50
50
  when "mysql2" then :mysql
51
+ when "sqlserver" then :sqlserver
51
52
  else default
52
53
  end
53
54
  else
@@ -17,44 +17,61 @@ module ScoutApm
17
17
  @installed
18
18
  end
19
19
 
20
+ def installed!
21
+ @installed = true
22
+ end
23
+
20
24
  def install
21
- # We previously instrumented ActionController::Metal, which missed
22
- # before and after filter timing. Instrumenting Base includes those
23
- # filters, at the expense of missing out on controllers that don't use
24
- # the full Rails stack.
25
- if defined?(::ActionController)
26
- @installed = true
25
+ if !defined?(::ActiveSupport)
26
+ return
27
+ end
28
+
29
+ # The block below runs with `self` equal to the ActionController::Base or ::API module, not this class we're in now. By saving an instance of ourselves into the `this` variable, we can continue accessing what we need.
30
+ this = self
31
+
32
+ ActiveSupport.on_load(:action_controller) do
33
+ if this.installed?
34
+ this.logger.info("Skipping ActionController - Already Ran")
35
+ next
36
+ else
37
+ this.logger.info("Instrumenting ActionController (on_load)")
38
+ this.installed!
39
+ end
27
40
 
41
+ # We previously instrumented ActionController::Metal, which missed
42
+ # before and after filter timing. Instrumenting Base includes those
43
+ # filters, at the expense of missing out on controllers that don't use
44
+ # the full Rails stack.
28
45
  if defined?(::ActionController::Base)
29
- logger.info "Instrumenting ActionController::Base"
46
+ this.logger.info "Instrumenting ActionController::Base"
30
47
  ::ActionController::Base.class_eval do
31
- # include ScoutApm::Tracer
32
48
  include ScoutApm::Instruments::ActionControllerBaseInstruments
33
49
  end
34
50
  end
35
51
 
36
52
  if defined?(::ActionController::Metal)
37
- logger.info "Instrumenting ActionController::Metal"
53
+ this.logger.info "Instrumenting ActionController::Metal"
38
54
  ::ActionController::Metal.class_eval do
39
55
  include ScoutApm::Instruments::ActionControllerMetalInstruments
40
56
  end
41
57
  end
42
58
 
43
59
  if defined?(::ActionController::API)
44
- logger.info "Instrumenting ActionController::Api"
60
+ this.logger.info "Instrumenting ActionController::Api"
45
61
  ::ActionController::API.class_eval do
46
62
  include ScoutApm::Instruments::ActionControllerAPIInstruments
47
63
  end
48
64
  end
49
65
  end
50
66
 
51
- # Returns a new anonymous module each time it is called. So
52
- # we can insert this multiple times into the ancestors
53
- # stack. Otherwise it only exists the first time you include it
54
- # (under Metal, instead of under API) and we miss instrumenting
55
- # before_action callbacks
67
+ ScoutApm::Agent.instance.context.logger.info("Instrumenting ActionController (hook installed)")
56
68
  end
57
69
 
70
+ # Returns a new anonymous module each time it is called. So
71
+ # we can insert this multiple times into the ancestors
72
+ # stack. Otherwise it only exists the first time you include it
73
+ # (under Metal, instead of under API) and we miss instrumenting
74
+ # before_action callbacks
58
75
  def self.build_instrument_module
59
76
  Module.new do
60
77
  def process_action(*args)
@@ -75,13 +75,18 @@ module ScoutApm
75
75
  end
76
76
 
77
77
  module ActionViewPartialRendererInstruments
78
+ # In Rails 6, the signature changed to pass the view & template args directly, as opposed to through the instance var
79
+ # New signature is: def render_partial(view, template)
78
80
  def render_partial(*args)
79
81
  req = ScoutApm::RequestManager.lookup
80
82
 
81
- template_name = @template.virtual_path rescue "Unknown Partial"
83
+ maybe_template = args[1]
84
+
85
+ template_name = @template.virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
86
+ template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.3
82
87
  template_name ||= "Unknown Partial"
83
- layer_name = template_name + "/Rendering"
84
88
 
89
+ layer_name = template_name + "/Rendering"
85
90
  layer = ScoutApm::Layer.new("View", layer_name)
86
91
  layer.subscopable!
87
92
 
@@ -25,7 +25,7 @@ module ScoutApm
25
25
  ::Net::HTTP.class_eval do
26
26
  include ScoutApm::Tracer
27
27
 
28
- def request_with_scout_instruments(*args,&block)
28
+ def request_with_scout_instruments(*args, &block)
29
29
  self.class.instrument("HTTP", "request", :ignore_children => true, :desc => request_scout_description(args.first)) do
30
30
  request_without_scout_instruments(*args, &block)
31
31
  end
@@ -35,8 +35,15 @@ module ScoutApm
35
35
  path = req.path
36
36
  path = path.path if path.respond_to?(:path)
37
37
 
38
+ # Protect against a nil address value
39
+ if @address.nil?
40
+ return "No Address Found"
41
+ end
42
+
38
43
  max_length = ScoutApm::Agent.instance.context.config.value('instrument_http_url_length')
39
44
  (@address + path.split('?').first)[0..(max_length - 1)]
45
+ rescue
46
+ ""
40
47
  end
41
48
 
42
49
  alias request_without_scout_instruments request
@@ -32,8 +32,10 @@ module ScoutApm
32
32
 
33
33
  # Modifies self and returns self, after merging in `other`.
34
34
  def combine!(other)
35
- same_job = queue_name == other.queue_name && job_name == other.job_name
36
- raise "Mismatched Merge of Background Job" unless same_job
35
+ if !self.eql?(other)
36
+ ScoutApm::Agent.instance.logger.debug("Mismatched Merge of Background Job: (Queue #{queue_name} == #{other.queue_name}) (Name #{job_name} == #{other.job_name}) (Hash #{hash} == #{other.hash})")
37
+ return self
38
+ end
37
39
 
38
40
  @errors += other.errors
39
41
  @metric_set = metric_set.combine!(other.metric_set)
@@ -30,6 +30,10 @@ module ScoutApm
30
30
 
31
31
  def serialize(data)
32
32
  Marshal.dump(data)
33
+ rescue
34
+ ScoutApm::Agent.instance.logger.info("Failed Marshalling LayawayFile")
35
+ ScoutApm::Agent.instance.logger.info(ScoutApm::Utils::MarshalLogging.new(data).dive) rescue nil
36
+ raise
33
37
  end
34
38
 
35
39
  def deserialize(data)
@@ -46,9 +46,15 @@ module ScoutApm
46
46
  set = child_set(metric_type)
47
47
 
48
48
  if set.size >= unique_cutoff
49
- # find limited_layer
50
- @limited_layers || init_limited_layers
51
- @limited_layers[metric_type].absorb(child)
49
+ # find or create limited_layer
50
+ @limited_layers ||= Hash.new
51
+ layer = if @limited_layers.has_key?(metric_type)
52
+ @limited_layers[metric_type]
53
+ else
54
+ @limited_layers[metric_type] = LimitedLayer.new(metric_type)
55
+ end
56
+
57
+ layer.absorb(child)
52
58
  else
53
59
  # we have space just add it
54
60
  set << child
@@ -76,10 +82,5 @@ module ScoutApm
76
82
  def size
77
83
  @children.size
78
84
  end
79
-
80
- # hold off initializing this until we know we need it
81
- def init_limited_layers
82
- @limited_layers ||= Hash.new { |hash, key| hash[key] = LimitedLayer.new(key) }
83
- end
84
85
  end
85
86
  end
@@ -58,6 +58,9 @@ module ScoutApm
58
58
  code = "" # User#index for instance
59
59
 
60
60
  spans = create_spans(request.root_layer)
61
+ if limited?
62
+ tags[:"scout.reached_span_cap"] = true
63
+ end
61
64
 
62
65
  DetailedTrace.new(
63
66
  transaction_id,
@@ -17,6 +17,10 @@ module ScoutApm
17
17
 
18
18
  def encode
19
19
  Marshal.dump(self)
20
+ rescue
21
+ ScoutApm::Agent.instance.logger.info("Failed Marshalling Remote::Message")
22
+ ScoutApm::Agent.instance.logger.info(ScoutApm::Utils::MarshalLogging.new(self).dive) rescue nil
23
+ raise
20
24
  end
21
25
  end
22
26
  end
@@ -5,6 +5,10 @@ module ScoutApm
5
5
  class AppServerLoadSerializer
6
6
  def self.serialize(data)
7
7
  Marshal.dump(data)
8
+ rescue
9
+ ScoutApm::Agent.instance.logger.info("Failed Marshalling AppServerLoad")
10
+ ScoutApm::Agent.instance.logger.info(ScoutApm::Utils::MarshalLogging.new(data).dive) rescue nil
11
+ raise
8
12
  end
9
13
 
10
14
  def self.deserialize(data)
@@ -5,6 +5,10 @@ module ScoutApm
5
5
  class DirectiveSerializer
6
6
  def self.serialize(data)
7
7
  Marshal.dump(data)
8
+ rescue
9
+ ScoutApm::Agent.instance.logger.info("Failed Marshalling Directive")
10
+ ScoutApm::Agent.instance.logger.info(ScoutApm::Utils::MarshalLogging.new(data).dive) rescue nil
11
+ raise
8
12
  end
9
13
 
10
14
  def self.deserialize(data)
@@ -0,0 +1,90 @@
1
+ module ScoutApm
2
+ module Utils
3
+ class Error < StandardError; end
4
+
5
+ class InstanceVar
6
+ attr_reader :name
7
+ attr_reader :obj
8
+
9
+ def initialize(name, obj, parent)
10
+ @name = name
11
+ @obj = obj
12
+ @parent = parent
13
+ end
14
+
15
+ def to_s
16
+ "#{@name} - #{obj.class}"
17
+ end
18
+
19
+ def history
20
+ (@parent.nil? ? [] : @parent.history) + [to_s]
21
+ end
22
+ end
23
+
24
+ class MarshalLogging
25
+ def initialize(base_obj)
26
+ @base_obj = base_obj
27
+ end
28
+
29
+ def dive
30
+ to_investigate = [InstanceVar.new('Root', @base_obj, nil)]
31
+ max_to_check = 10000
32
+ checked = 0
33
+
34
+ while (var = to_investigate.shift)
35
+ checked += 1
36
+ if checked > max_to_check
37
+ return "Limiting Checks (max = #{max_to_check})"
38
+ end
39
+
40
+ obj = var.obj
41
+
42
+ if offending_hash?(obj)
43
+ return "Found undumpable object: #{var.history}"
44
+ end
45
+
46
+ if !dumps?(obj)
47
+ if obj.is_a? Hash
48
+ keys = obj.keys
49
+ keys.each do |key|
50
+ to_investigate.push(
51
+ InstanceVar.new(key.to_s, obj[key], var)
52
+ )
53
+ end
54
+ elsif obj.is_a? Array
55
+ obj.each_with_index do |value, idx|
56
+ to_investigate.push(
57
+ InstanceVar.new("Index #{idx}", value, var)
58
+ )
59
+ end
60
+ else
61
+ symbols = obj.instance_variables
62
+ if !symbols.any?
63
+ return "Found undumpable object: #{var.history}"
64
+ end
65
+
66
+ symbols.each do |sym|
67
+ to_investigate.push(
68
+ InstanceVar.new(sym, obj.instance_variable_get(sym), var)
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ true
76
+ end
77
+
78
+ def dumps?(obj)
79
+ Marshal.dump(obj)
80
+ true
81
+ rescue TypeError
82
+ false
83
+ end
84
+
85
+ def offending_hash?(obj)
86
+ obj.is_a?(Hash) && !obj.default_proc.nil?
87
+ end
88
+ end
89
+ end
90
+ end
@@ -34,15 +34,23 @@ module ScoutApm
34
34
  when :postgres then to_s_postgres
35
35
  when :mysql then to_s_mysql
36
36
  when :sqlite then to_s_sqlite
37
+ when :sqlserver then to_s_sqlserver
37
38
  end
38
39
  end
39
40
 
40
41
  private
41
42
 
43
+ def to_s_sqlserver
44
+ sql.gsub!(SQLSERVER_EXECUTESQL, '\1')
45
+ sql.gsub!(SQLSERVER_REMOVE_INTEGERS, '?')
46
+ sql.gsub!(SQLSERVER_IN_CLAUSE, 'IN (?)')
47
+ sql
48
+ end
49
+
42
50
  def to_s_postgres
43
51
  sql.gsub!(PSQL_PLACEHOLDER, '?')
44
52
  sql.gsub!(PSQL_VAR_INTERPOLATION, '')
45
- sql.gsub!(PSQL_REMOVE_STRINGS, '?')
53
+ sql.gsub!(PSQL_AFTER_WHERE) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
46
54
  sql.gsub!(PSQL_REMOVE_INTEGERS, '?')
47
55
  sql.gsub!(PSQL_IN_CLAUSE, 'IN (?)')
48
56
  sql.gsub!(MULTIPLE_SPACES, ' ')
@@ -5,11 +5,13 @@ module ScoutApm
5
5
  MULTIPLE_SPACES = %r|\s+|.freeze
6
6
  MULTIPLE_QUESTIONS = /\?(,\?)+/.freeze
7
7
 
8
+
8
9
  PSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
9
10
  PSQL_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
10
11
  PSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
11
12
  PSQL_PLACEHOLDER = /\$\d+/.freeze
12
13
  PSQL_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
14
+ PSQL_AFTER_WHERE = /(?:WHERE\s+).*?(?:SELECT|$)/i.freeze
13
15
 
14
16
  MYSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
15
17
  MYSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
@@ -20,6 +22,11 @@ module ScoutApm
20
22
  SQLITE_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
21
23
  SQLITE_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
22
24
  SQLITE_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
25
+
26
+ # => "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE (age > 50) ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY', N'@0 int', @0 = 10"
27
+ SQLSERVER_EXECUTESQL = /EXEC sp_executesql N'(.*?)'.*/
28
+ SQLSERVER_REMOVE_INTEGERS = /(?<!LIMIT )\b(?<!@)\d+\b/.freeze
29
+ SQLSERVER_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
23
30
  end
24
31
  end
25
32
  end
@@ -10,6 +10,7 @@ module ScoutApm
10
10
  PSQL_REMOVE_INTEGERS = /\b\d+\b/.freeze
11
11
  PSQL_PLACEHOLDER = /\$\d+/.freeze
12
12
  PSQL_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
13
+ PSQL_AFTER_WHERE = /(?:WHERE\s+).*?(?:SELECT|$)/i.freeze
13
14
 
14
15
  MYSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
15
16
  MYSQL_REMOVE_INTEGERS = /\b\d+\b/.freeze
@@ -21,6 +22,10 @@ module ScoutApm
21
22
  SQLITE_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
22
23
  SQLITE_REMOVE_INTEGERS = /\b\d+\b/.freeze
23
24
 
25
+ # This is not officially supported, but will do its best.
26
+ SQLSERVER_EXECUTESQL = /EXEC sp_executesql N'(.*?)'.*/
27
+ SQLSERVER_REMOVE_INTEGERS = /\b\d+\b/.freeze
28
+ SQLSERVER_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
24
29
  end
25
30
  end
26
31
  end
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "2.6.3"
2
+ VERSION = "2.6.8"
3
3
  end
@@ -5,7 +5,7 @@ SimpleCov.start
5
5
  require 'minitest/autorun'
6
6
  require 'minitest/unit'
7
7
  require 'minitest/pride'
8
- require 'mocha/mini_test'
8
+ require 'mocha/minitest'
9
9
  require 'pry'
10
10
 
11
11
 
@@ -71,6 +71,15 @@ class LayerChildrenSetTest < Minitest::Test
71
71
  limited_layers.each { |ml| assert_equal 5, ml.count }
72
72
  end
73
73
 
74
+ def test_works_with_marshal
75
+ s = SET.new(5)
76
+ 10.times do
77
+ s << make_layer("LayerType", "LayerName")
78
+ end
79
+
80
+ Marshal.dump(s)
81
+ end
82
+
74
83
  #############
75
84
  # Helpers #
76
85
  #############
@@ -28,7 +28,23 @@ module ScoutApm
28
28
  sql = %q|SELECT "users".* FROM "users" INNER JOIN "blogs" ON "blogs"."user_id" = "users"."id" WHERE (blogs.title = 'hello world')|
29
29
  ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
30
30
  assert_equal %q|SELECT "users".* FROM "users" INNER JOIN "blogs" ON "blogs"."user_id" = "users"."id" WHERE (blogs.title = ?)|, ss.to_s
31
+ end
32
+
33
+ def test_postgres_strips_after_where
34
+ raw_sql = %q|SELECT DISTINCT ON (flagged_traces.metric_name) flagged_traces.metric_name, "flagged_traces"."trace_id", "flagged_traces"."trace_type", "flagged_traces"."trace_occurred_at", flagged_traces.details ->> 'uri' as uri, (flagged_traces.details ->> 'n_sum_millis')::float as potential_savings, (flagged_traces.details ->> 'n_count')::float as num_queries FROM "flagged_traces" WHERE "flagged_traces"."app_id" = 5 AND "flagged_traces"."trace_type" = 'Request' AND ("flagged_traces"."trace_occurred_at" BETWEEN '2019-04-17 12:28:00.000000' AND '2019-04-18 12:28:00.000000') AND "flagged_traces"."flag_type" = 'nplusone' ORDER BY "flagged_traces"."metric_name" ASC, potential_savings DESC|
35
+ sanitized_sql = SqlSanitizer.new(raw_sql).tap { |it| it.database_engine = :postgres}
36
+ expected_sql = %q|SELECT DISTINCT ON (flagged_traces.metric_name) flagged_traces.metric_name, "flagged_traces"."trace_id", "flagged_traces"."trace_type", "flagged_traces"."trace_occurred_at", flagged_traces.details ->> 'uri' as uri, (flagged_traces.details ->> 'n_sum_millis')::float as potential_savings, (flagged_traces.details ->> 'n_count')::float as num_queries FROM "flagged_traces" WHERE "flagged_traces"."app_id" = ? AND "flagged_traces"."trace_type" = ? AND ("flagged_traces"."trace_occurred_at" BETWEEN ? AND ?) AND "flagged_traces"."flag_type" = ? ORDER BY "flagged_traces"."metric_name" ASC, potential_savings DESC|
37
+ assert_equal expected_sql, sanitized_sql.to_s
38
+ end
31
39
 
40
+ def test_postgres_strips_subquery_strings
41
+ raw_sql = %q|"SELECT 'orgs'.* FROM "orgs" WHERE "orgs"."name" = 'Scout' AND "orgs"."created_by_user_id" IN (SELECT 'users'.'id' FROM "users" WHERE (id > AVG(id)) AND "type" = 'USER' AND "created_at" BETWEEN '2019-04-17 12:28:00.000000' AND '2019-04-18 12:28:00.000000')"|
42
+ sanitized_sql = SqlSanitizer.new(raw_sql).tap { |it| it.database_engine = :postgres}
43
+ expected_sql = %q|"SELECT 'orgs'.* FROM "orgs" WHERE "orgs"."name" = ? AND "orgs"."created_by_user_id" IN (SELECT 'users'.'id' FROM "users" WHERE (id > AVG(id)) AND "type" = ? AND "created_at" BETWEEN ? AND ?)"|
44
+ assert_equal expected_sql, sanitized_sql.to_s
45
+ end
46
+
47
+ def test_postgres_strips_integers
32
48
  # Strip integers
33
49
  sql = %q|SELECT "blogs".* FROM "blogs" WHERE (view_count > 10)|
34
50
  ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
@@ -89,6 +105,30 @@ module ScoutApm
89
105
  assert_equal %q|INSERT INTO `users` VALUES (?, ?)|, ss.to_s
90
106
  end
91
107
 
108
+ def test_sqlserver_integers
109
+ skip "SQLServer Support requires Ruby 1.9+ For Regexes"
110
+
111
+ sql = "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE (age > 50) ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY', N'@0 int', @0 = 10"
112
+ ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :sqlserver }
113
+ assert_equal %q|SELECT [users].* FROM [users] WHERE (age > ?) ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @0 ROWS ONLY|, ss.to_s
114
+ end
115
+
116
+ def test_sqlserver_strings
117
+ skip "SQLServer Support requires Ruby 1.9+ For Regexes"
118
+
119
+ sql = "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE [users].[email] = @0 ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @1 ROWS ONLY', N'@0 nvarchar(4000), @1 int', @0 = N'foo', @1 = 10"
120
+ ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :sqlserver }
121
+ assert_equal %q|SELECT [users].* FROM [users] WHERE [users].[email] = @0 ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @1 ROWS ONLY|, ss.to_s
122
+ end
123
+
124
+ def test_sqlserver_in_clause
125
+ skip "SQLServer Support requires Ruby 1.9+ For Regexes"
126
+
127
+ sql = "EXEC sp_executesql N'SELECT [users].* FROM [users] WHERE (id IN (1,2,3)) ORDER BY [users].[id] ASC OFFSET 0 ROWS FETCH NEXT @0 ROWS ONLY', N'@0 int', @0 = 10"
128
+ ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :sqlserver }
129
+ assert_equal %q|SELECT [users].* FROM [users] WHERE (id IN (?)) ORDER BY [users].[id] ASC OFFSET ? ROWS FETCH NEXT @0 ROWS ONLY|, ss.to_s
130
+ end
131
+
92
132
  def test_scrubs_invalid_encoding
93
133
  skip "Ruby 1.8.7 has no concept of encoding" if RUBY_VERSION.start_with?("1.8.")
94
134
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.3
4
+ version: 2.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-10-31 00:00:00.000000000 Z
12
+ date: 2020-06-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -250,6 +250,7 @@ files:
250
250
  - lib/scout_apm/auto_instrument/parser.rb
251
251
  - lib/scout_apm/auto_instrument/rails.rb
252
252
  - lib/scout_apm/background_job_integrations/delayed_job.rb
253
+ - lib/scout_apm/background_job_integrations/legacy_sneakers.rb
253
254
  - lib/scout_apm/background_job_integrations/que.rb
254
255
  - lib/scout_apm/background_job_integrations/resque.rb
255
256
  - lib/scout_apm/background_job_integrations/shoryuken.rb
@@ -280,7 +281,6 @@ files:
280
281
  - lib/scout_apm/instant/middleware.rb
281
282
  - lib/scout_apm/instant_reporting.rb
282
283
  - lib/scout_apm/instrument_manager.rb
283
- - lib/scout_apm/instruments/.DS_Store
284
284
  - lib/scout_apm/instruments/action_controller_rails_2.rb
285
285
  - lib/scout_apm/instruments/action_controller_rails_3_rails4.rb
286
286
  - lib/scout_apm/instruments/action_view.rb
@@ -376,6 +376,7 @@ files:
376
376
  - lib/scout_apm/utils/gzip_helper.rb
377
377
  - lib/scout_apm/utils/installed_gems.rb
378
378
  - lib/scout_apm/utils/klass_helper.rb
379
+ - lib/scout_apm/utils/marshal_logging.rb
379
380
  - lib/scout_apm/utils/numbers.rb
380
381
  - lib/scout_apm/utils/scm.rb
381
382
  - lib/scout_apm/utils/sql_sanitizer.rb
@@ -459,61 +460,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
459
460
  - !ruby/object:Gem::Version
460
461
  version: '0'
461
462
  requirements: []
462
- rubygems_version: 3.0.4
463
+ rubygems_version: 3.0.8
463
464
  signing_key:
464
465
  specification_version: 4
465
466
  summary: Ruby application performance monitoring
466
- test_files:
467
- - test/data/config_test_1.yml
468
- - test/test_helper.rb
469
- - test/tmp/README.md
470
- - test/unit/agent_test.rb
471
- - test/unit/auto_instrument/assignments-instrumented.rb
472
- - test/unit/auto_instrument/assignments.rb
473
- - test/unit/auto_instrument/controller-ast.txt
474
- - test/unit/auto_instrument/controller-instrumented.rb
475
- - test/unit/auto_instrument/controller.rb
476
- - test/unit/auto_instrument/rescue_from-instrumented.rb
477
- - test/unit/auto_instrument/rescue_from.rb
478
- - test/unit/auto_instrument_test.rb
479
- - test/unit/background_job_integrations/sidekiq_test.rb
480
- - test/unit/config_test.rb
481
- - test/unit/context_test.rb
482
- - test/unit/db_query_metric_set_test.rb
483
- - test/unit/db_query_metric_stats_test.rb
484
- - test/unit/environment_test.rb
485
- - test/unit/extensions/periodic_callbacks_test.rb
486
- - test/unit/extensions/transaction_callbacks_test.rb
487
- - test/unit/fake_store_test.rb
488
- - test/unit/git_revision_test.rb
489
- - test/unit/histogram_test.rb
490
- - test/unit/ignored_uris_test.rb
491
- - test/unit/instruments/active_record_test.rb
492
- - test/unit/instruments/net_http_test.rb
493
- - test/unit/instruments/percentile_sampler_test.rb
494
- - test/unit/layaway_test.rb
495
- - test/unit/layer_children_set_test.rb
496
- - test/unit/layer_converters/depth_first_walker_test.rb
497
- - test/unit/layer_converters/metric_converter_test.rb
498
- - test/unit/layer_converters/stubs.rb
499
- - test/unit/limited_layer_test.rb
500
- - test/unit/logger_test.rb
501
- - test/unit/metric_set_test.rb
502
- - test/unit/remote/test_message.rb
503
- - test/unit/remote/test_router.rb
504
- - test/unit/remote/test_server.rb
505
- - test/unit/request_histograms_test.rb
506
- - test/unit/scored_item_set_test.rb
507
- - test/unit/serializers/payload_serializer_test.rb
508
- - test/unit/slow_job_policy_test.rb
509
- - test/unit/slow_request_policy_test.rb
510
- - test/unit/sql_sanitizer_test.rb
511
- - test/unit/store_test.rb
512
- - test/unit/tracer_test.rb
513
- - test/unit/tracked_request_test.rb
514
- - test/unit/transaction_test.rb
515
- - test/unit/transaction_time_consumed_test.rb
516
- - test/unit/utils/active_record_metric_name_test.rb
517
- - test/unit/utils/backtrace_parser_test.rb
518
- - test/unit/utils/numbers_test.rb
519
- - test/unit/utils/scm.rb
467
+ test_files: []