scout_apm 2.6.4 → 2.6.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +34 -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/config.rb +3 -1
  11. data/lib/scout_apm/detailed_trace.rb +2 -1
  12. data/lib/scout_apm/extensions/transaction_callback_payload.rb +1 -1
  13. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +1 -0
  14. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +47 -26
  15. data/lib/scout_apm/instruments/action_view.rb +7 -2
  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/reporter.rb +1 -2
  22. data/lib/scout_apm/serializers/app_server_load_serializer.rb +4 -0
  23. data/lib/scout_apm/serializers/directive_serializer.rb +4 -0
  24. data/lib/scout_apm/utils/marshal_logging.rb +90 -0
  25. data/lib/scout_apm/utils/sql_sanitizer.rb +10 -1
  26. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +7 -0
  27. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +6 -0
  28. data/lib/scout_apm/version.rb +1 -1
  29. data/test/test_helper.rb +1 -1
  30. data/test/unit/layer_children_set_test.rb +9 -0
  31. data/test/unit/sql_sanitizer_test.rb +47 -0
  32. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96f6a10ca19691ef0b5feb036811700759fbd6164a0811efbe52b1fcaa97b6c9
4
- data.tar.gz: c2c39bcb738f22115d0a0f69f40340a5096f523e542c844fe007b15f42a324d1
3
+ metadata.gz: 54f1c7e07f92a0d5d67a22354b0273c4894e8408ec00bcdada275457ed2f00f2
4
+ data.tar.gz: 50cf2d441c948e769b2f2222895c84aadb0a5e3e3f75d16e647a2096a86551e4
5
5
  SHA512:
6
- metadata.gz: ea5173420001f92d54aae2a05b7ca71287ca08b3da08b27769895b9ddc0aca791dfb226c93ed84464728cc02a7c6defc486b3b1d606e58f64323d7c523dfdbfb
7
- data.tar.gz: 91b7ace32216ea11bffa119d782b3f43dba518fc1adf07bae866e20c8cf83e25e055ded16fd57ee2d6dc7d1c2f4b92c88849ab207adf316435d520b016bf7e3a
6
+ metadata.gz: 27a012457a6871cdbed206f2a55914163571e81af1b1f6253079c2fe9954354a9888b59627d832f3635a536ddf3f331c066b2bc4e053956f5135c706ffea1a17
7
+ data.tar.gz: cfc5d80fb3b0ccebc2e78246a394a23a8e16d52a89868cff024931d6ce5b3539aafc42ef6f491105db43f8c544750b270cbbc7cc1fed556ab543bb0d963f1c75
@@ -1,3 +1,36 @@
1
+ # 2.6.9
2
+
3
+ * Add `ssl_cert_file` config option (#352)
4
+ * Improve sanitization of Postgres UPDATE SQL (#351)
5
+ * Allow custom URL sanitization (#341)
6
+
7
+ # 2.6.8
8
+
9
+ * Lock rake version for 1.8.7 to older version (#329)
10
+ * Delete unneeded .DS_Store file that snuck in (#334)
11
+ * Fix typo in "queue_time_ms"
12
+ * Fix Rails 6 deprecation warning at boot time (#337)
13
+ * Fix partial naming on Rails 6.0 (#339)
14
+ * Support Sidekiq 6.1 instrumentation (#340)
15
+
16
+ # 2.6.7
17
+
18
+ * Remove accidental call to `as_json`
19
+
20
+ # 2.6.6
21
+
22
+ * Add basic support for parsing Microsoft SQLServer queries (#317)
23
+ * Refine Postgresql Sanitization with subqueries and JSON operations (#262)
24
+
25
+ # 2.6.5
26
+
27
+ * Add a tag to any requests that reach maximum number of spans (#316)
28
+ * Update testing library Mocha (#315)
29
+ * Fix case sensitivity mismatch in Job renaming (#314)
30
+ * Add support for Sneakers 2.5 (#313)
31
+ * Fix edge case with Resque instrumentation (#312)
32
+ * Fix missing source code when used with BugSnag (#308)
33
+
1
34
  # 2.6.4
2
35
 
3
36
  * Add defensive check against a nil @address in Net/HTTP instruments (#306)
@@ -26,6 +59,7 @@
26
59
  # 2.5.3
27
60
 
28
61
  * Add Que support (#265)
62
+ * Add Memcached support (#279)
29
63
 
30
64
  # 2.5.2
31
65
 
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
@@ -75,6 +75,7 @@ module ScoutApm
75
75
  'revision_sha',
76
76
  'scm_subdirectory',
77
77
  'start_resque_server_instrument',
78
+ 'ssl_cert_file',
78
79
  'uri_reporting',
79
80
  'instrument_http_url_length',
80
81
  'timeline_traces',
@@ -284,7 +285,8 @@ module ScoutApm
284
285
  'collect_remote_ip' => true,
285
286
  'timeline_traces' => true,
286
287
  'auto_instruments' => false,
287
- 'auto_instruments_ignore' => []
288
+ 'auto_instruments_ignore' => [],
289
+ 'ssl_cert_file' => File.join( File.dirname(__FILE__), *%w[.. .. data cacert.pem] )
288
290
  }.freeze
289
291
 
290
292
  def value(key)
@@ -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,46 +17,73 @@ 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
27
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
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
77
+ # Determine the URI of this request to capture. Overridable by users in their controller.
78
+ def scout_transaction_uri(config=ScoutApm::Agent.instance.context.config)
79
+ case config.value("uri_reporting")
80
+ when 'path'
81
+ request.path # strips off the query string for more security
82
+ else # default handles filtered params
83
+ request.filtered_path
84
+ end
85
+ end
86
+
60
87
  def process_action(*args)
61
88
  req = ScoutApm::RequestManager.lookup
62
89
  current_layer = req.current_layer
@@ -72,7 +99,11 @@ module ScoutApm
72
99
  # Don't start a new layer if ActionController::API or ActionController::Base handled it already.
73
100
  super
74
101
  else
75
- req.annotate_request(:uri => ScoutApm::Instruments::ActionControllerRails3Rails4.scout_transaction_uri(request))
102
+ begin
103
+ uri = scout_transaction_uri
104
+ req.annotate_request(:uri => uri)
105
+ rescue
106
+ end
76
107
 
77
108
  # IP Spoofing Protection can throw an exception, just move on w/o remote ip
78
109
  if agent_context.config.value('collect_remote_ip')
@@ -95,16 +126,6 @@ module ScoutApm
95
126
  end
96
127
  end
97
128
 
98
- # Given an +ActionDispatch::Request+, formats the uri based on config settings.
99
- # XXX: Don't lookup context like this - find a way to pass it through
100
- def self.scout_transaction_uri(request, config=ScoutApm::Agent.instance.context.config)
101
- case config.value("uri_reporting")
102
- when 'path'
103
- request.path # strips off the query string for more security
104
- else # default handles filtered params
105
- request.filtered_path
106
- end
107
- end
108
129
  end
109
130
 
110
131
  module ActionControllerMetalInstruments
@@ -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
 
@@ -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
@@ -2,7 +2,6 @@ require 'openssl'
2
2
 
3
3
  module ScoutApm
4
4
  class Reporter
5
- CA_FILE = File.join( File.dirname(__FILE__), *%w[.. .. data cacert.pem] )
6
5
  VERIFY_MODE = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
7
6
 
8
7
  attr_reader :type
@@ -123,7 +122,7 @@ module ScoutApm
123
122
  proxy_uri.password).new(url.host, url.port)
124
123
  if url.is_a?(URI::HTTPS)
125
124
  http.use_ssl = true
126
- http.ca_file = CA_FILE
125
+ http.ca_file = config.value("ssl_cert_file")
127
126
  http.verify_mode = VERIFY_MODE
128
127
  end
129
128
  http
@@ -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,24 @@ 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, '?')}
54
+ sql.gsub!(PSQL_AFTER_SET) {|c| c.gsub(PSQL_REMOVE_STRINGS, '?')}
46
55
  sql.gsub!(PSQL_REMOVE_INTEGERS, '?')
47
56
  sql.gsub!(PSQL_IN_CLAUSE, 'IN (?)')
48
57
  sql.gsub!(MULTIPLE_SPACES, ' ')
@@ -10,6 +10,8 @@ module ScoutApm
10
10
  PSQL_REMOVE_INTEGERS = /(?<!LIMIT )\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
14
+ PSQL_AFTER_SET = /(?:SET\s+).*?(?:WHERE|$)/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,8 @@ 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
14
+ PSQL_AFTER_SET = /(?:SET\s+).*?(?:WHERE|$)/i.freeze
13
15
 
14
16
  MYSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
15
17
  MYSQL_REMOVE_INTEGERS = /\b\d+\b/.freeze
@@ -21,6 +23,10 @@ module ScoutApm
21
23
  SQLITE_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
22
24
  SQLITE_REMOVE_INTEGERS = /\b\d+\b/.freeze
23
25
 
26
+ # This is not officially supported, but will do its best.
27
+ SQLSERVER_EXECUTESQL = /EXEC sp_executesql N'(.*?)'.*/
28
+ SQLSERVER_REMOVE_INTEGERS = /\b\d+\b/.freeze
29
+ SQLSERVER_IN_CLAUSE = /IN\s+\(\?[^\)]*\)/.freeze
24
30
  end
25
31
  end
26
32
  end
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "2.6.4"
2
+ VERSION = "2.6.9"
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
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
31
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
 
@@ -99,6 +139,13 @@ module ScoutApm
99
139
  assert_equal %q|SELECT `blogs`.* FROM `blogs` WHERE (title = ?)|, ss.to_s
100
140
  end
101
141
 
142
+ def test_set_columns
143
+ sql = %q|UPDATE "mytable" SET "myfield" = 'fieldcontent', "countofthings" = 10 WHERE "user_id" = 10|
144
+
145
+ ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
146
+ assert_equal %q|UPDATE "mytable" SET "myfield" = ?, "countofthings" = ? WHERE "user_id" = ?|, ss.to_s
147
+ end
148
+
102
149
  def assert_faster_than(target_seconds)
103
150
  t1 = ::Time.now
104
151
  yield
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.4
4
+ version: 2.6.9
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-11-14 00:00:00.000000000 Z
12
+ date: 2020-08-17 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,7 +460,7 @@ 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.6
463
464
  signing_key:
464
465
  specification_version: 4
465
466
  summary: Ruby application performance monitoring