scout_apm 1.6.8 → 2.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -1
  3. data/CHANGELOG.markdown +7 -57
  4. data/ext/allocations/allocations.c +84 -0
  5. data/ext/allocations/extconf.rb +3 -0
  6. data/lib/scout_apm/agent/reporting.rb +9 -32
  7. data/lib/scout_apm/agent.rb +45 -31
  8. data/lib/scout_apm/app_server_load.rb +1 -2
  9. data/lib/scout_apm/attribute_arranger.rb +0 -4
  10. data/lib/scout_apm/background_worker.rb +6 -9
  11. data/lib/scout_apm/bucket_name_splitter.rb +3 -3
  12. data/lib/scout_apm/call_set.rb +1 -0
  13. data/lib/scout_apm/config.rb +110 -66
  14. data/lib/scout_apm/environment.rb +16 -10
  15. data/lib/scout_apm/framework_integrations/rails_2.rb +12 -14
  16. data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +5 -17
  17. data/lib/scout_apm/framework_integrations/ruby.rb +0 -4
  18. data/lib/scout_apm/framework_integrations/sinatra.rb +0 -4
  19. data/lib/scout_apm/histogram.rb +0 -20
  20. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +1 -4
  21. data/lib/scout_apm/instruments/active_record.rb +149 -8
  22. data/lib/scout_apm/instruments/mongoid.rb +5 -78
  23. data/lib/scout_apm/instruments/process/process_cpu.rb +0 -12
  24. data/lib/scout_apm/instruments/process/process_memory.rb +14 -43
  25. data/lib/scout_apm/layaway.rb +34 -134
  26. data/lib/scout_apm/layaway_file.rb +50 -27
  27. data/lib/scout_apm/layer.rb +45 -1
  28. data/lib/scout_apm/layer_converters/allocation_metric_converter.rb +17 -0
  29. data/lib/scout_apm/layer_converters/converter_base.rb +4 -6
  30. data/lib/scout_apm/layer_converters/job_converter.rb +1 -0
  31. data/lib/scout_apm/layer_converters/metric_converter.rb +2 -1
  32. data/lib/scout_apm/layer_converters/slow_job_converter.rb +42 -21
  33. data/lib/scout_apm/layer_converters/slow_request_converter.rb +58 -37
  34. data/lib/scout_apm/metric_meta.rb +1 -5
  35. data/lib/scout_apm/metric_set.rb +6 -15
  36. data/lib/scout_apm/reporter.rb +4 -6
  37. data/lib/scout_apm/serializers/metrics_to_json_serializer.rb +5 -1
  38. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +1 -3
  39. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +5 -3
  40. data/lib/scout_apm/slow_job_policy.rb +19 -89
  41. data/lib/scout_apm/slow_job_record.rb +12 -20
  42. data/lib/scout_apm/slow_request_policy.rb +12 -80
  43. data/lib/scout_apm/slow_transaction.rb +16 -20
  44. data/lib/scout_apm/stackprof_tree_collapser.rb +103 -0
  45. data/lib/scout_apm/store.rb +16 -78
  46. data/lib/scout_apm/tracked_request.rb +53 -36
  47. data/lib/scout_apm/utils/active_record_metric_name.rb +2 -0
  48. data/lib/scout_apm/utils/fake_stack_prof.rb +40 -0
  49. data/lib/scout_apm/utils/klass_helper.rb +26 -0
  50. data/lib/scout_apm/utils/sql_sanitizer.rb +1 -1
  51. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +2 -2
  52. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +2 -2
  53. data/lib/scout_apm/version.rb +1 -1
  54. data/lib/scout_apm.rb +13 -7
  55. data/scout_apm.gemspec +3 -1
  56. data/test/test_helper.rb +3 -4
  57. data/test/unit/layaway_test.rb +8 -5
  58. data/test/unit/serializers/payload_serializer_test.rb +2 -2
  59. data/test/unit/slow_item_set_test.rb +1 -2
  60. data/test/unit/sql_sanitizer_test.rb +0 -6
  61. metadata +28 -20
  62. data/LICENSE.md +0 -27
  63. data/lib/scout_apm/instruments/grape.rb +0 -69
  64. data/lib/scout_apm/instruments/percentile_sampler.rb +0 -37
  65. data/lib/scout_apm/request_histograms.rb +0 -46
  66. data/lib/scout_apm/scored_item_set.rb +0 -79
  67. data/test/unit/metric_set_test.rb +0 -101
  68. data/test/unit/scored_item_set_test.rb +0 -65
  69. data/test/unit/slow_request_policy_test.rb +0 -42
@@ -7,103 +7,147 @@ require 'scout_apm/environment'
7
7
  #
8
8
  # application_root - override the detected directory of the application
9
9
  # data_file - override the default temporary storage location. Must be a location in a writable directory
10
- # host - override the default hostname detection. Default varies by environment - either system hostname, or PAAS hostname
11
- # direct_host - override the default "direct" host. The direct_host bypasses the ingestion pipeline and goes directly to the webserver, and is primarily used for features under development.
10
+ # hostname - override the default hostname detection. Default varies by environment - either system hostname, or PAAS hostname
12
11
  # key - the account key with Scout APM. Found in Settings in the Web UI
13
12
  # log_file_path - either a directory or "STDOUT".
14
13
  # log_level - DEBUG / INFO / WARN as usual
15
14
  # monitor - true or false. False prevents any instrumentation from starting
16
15
  # name - override the name reported to APM. This is the name that shows in the Web UI
17
16
  # uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
18
- # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
17
+ # report_format - 'json' or 'marshal'. Json is default, Marshal is deprecated
19
18
  #
20
19
  # Any of these config settings can be set with an environment variable prefixed
21
20
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
22
21
 
22
+
23
+ # Config - Made up of config overlay
24
+ # Default -> File -> Environment Var
25
+ # QUESTION: How to embed arrays or hashes into ENV?
26
+
23
27
  module ScoutApm
24
28
  class Config
25
- DEFAULTS = {
26
- 'host' => 'https://checkin.scoutapp.com',
27
- 'direct_host' => 'https://apm.scoutapp.com',
28
- 'log_level' => 'info',
29
- 'uri_reporting' => 'full_path',
30
- 'report_format' => 'json',
31
- 'disabled_instruments' => [],
32
- 'enable_background_jobs' => true,
33
- 'ignore_traces' => [],
34
- }.freeze
35
-
36
- def initialize(config_path = nil)
37
- @config_path = config_path
29
+ # Load up a config instance without attempting to load a file.
30
+ # Useful for bootstrapping.
31
+ def self.without_file
32
+ overlays = [
33
+ ConfigEnvironment.new,
34
+ ConfigDefaults.new,
35
+ ]
36
+ new(overlays)
38
37
  end
39
38
 
40
- def config_file_exists?
41
- File.exist?(config_path)
39
+ # Load up a config instance, attempting to load a yaml file. Allows a
40
+ # definite location if requested, or will attempt to load the default
41
+ # configuration file: APP_ROOT/config/scout_apm.yml
42
+ def self.with_file(file_path=nil)
43
+ overlays = [
44
+ ConfigEnvironment.new,
45
+ ConfigFile.new(file_path),
46
+ ConfigDefaults.new,
47
+ ]
48
+ new(overlays)
42
49
  end
43
50
 
44
- # Fetch a config value.
45
- # It first attempts to fetch an ENV var prefixed with 'SCOUT_',
46
- # then from the settings file.
47
- #
48
- # If you set env_only, then it will not attempt to read the config file at
49
- # all, and only read off the ENV var this is useful to break a loop during
50
- # boot, where we needed an option to set the application root.
51
- def value(key, env_only=false)
52
- value = if env_only
53
- ENV['SCOUT_' + key.upcase]
54
- else
55
- ENV['SCOUT_' + key.upcase] || setting(key)
56
- end
57
-
58
- value.to_s.strip.length.zero? ? nil : value
51
+ def initialize(overlays)
52
+ @overlays = overlays
59
53
  end
60
54
 
61
- private
55
+ def value(key)
56
+ @overlays.each do |overlay|
57
+ if result = overlay.value(key)
58
+ return result
59
+ end
60
+ end
62
61
 
63
- def config_path
64
- @config_path || File.join(ScoutApm::Environment.instance.root, "config", "scout_apm.yml")
62
+ nil
65
63
  end
66
64
 
67
- def config_file
68
- File.expand_path(config_path)
69
- end
65
+ class ConfigDefaults
66
+ DEFAULTS = {
67
+ 'host' => 'https://checkin.scoutapp.com',
68
+ 'log_level' => 'info',
69
+ 'stackprof_interval' => 20000, # microseconds, 1000 = 1 millisecond, so 20k == 20 milliseconds
70
+ 'uri_reporting' => 'full_path',
71
+ 'report_format' => 'json',
72
+ 'disabled_instruments' => [],
73
+ 'enable_background_jobs' => true,
74
+ }.freeze
70
75
 
71
- def setting(key)
72
- settings[key] || settings(true)[key]
76
+ def value(key)
77
+ DEFAULTS[key]
78
+ end
73
79
  end
74
80
 
75
- def settings(try_reload=false)
76
- (@settings.nil? || try_reload) ? @settings = load_file : @settings
81
+ class ConfigEnvironment
82
+ def value(key)
83
+ val = ENV['SCOUT_' + key.upcase]
84
+ val.to_s.strip.length.zero? ? nil : val
85
+ end
77
86
  end
78
87
 
79
- def config_environment
80
- @config_environment ||= ScoutApm::Environment.instance.env
81
- end
88
+ # Attempts to load a configuration file, and parse it as YAML. If the file
89
+ # is not found, inaccessbile, or unparsable, log a message to that effect,
90
+ # and move on.
91
+ class ConfigFile
92
+ def initialize(file_path=nil)
93
+ @resolved_file_path = file_path || determine_file_path
94
+ load_file(@resolved_file_path)
95
+ end
82
96
 
83
- def load_file
84
- settings_hash = {}
85
- begin
86
- if File.exist?(config_file)
87
- settings_hash = YAML.load(ERB.new(File.read(config_file)).result(binding))[config_environment] || {}
97
+ def value(key)
98
+ if @file_loaded
99
+ val = @settings[key]
100
+ val.to_s.strip.length.zero? ? nil : val
88
101
  else
89
- logger.warn "No config file found at [#{config_file}]."
102
+ nil
90
103
  end
91
- rescue Exception => e
92
- logger.warn "Unable to load the config file."
93
- logger.warn e.message
94
- logger.warn e.backtrace
95
104
  end
96
- DEFAULTS.merge(settings_hash)
97
- end
98
105
 
99
- # if we error out early enough, we don't have access to ScoutApm's logger
100
- # in that case, be silent unless ENV['SCOUT_DEBUG'] is set, then STDOUT it
101
- def logger
102
- if defined?(ScoutApm::Agent) && (apm_log = ScoutApm::Agent.instance.logger)
103
- apm_log
104
- else
105
- require 'scout_apm/utils/null_logger'
106
- ENV['SCOUT_DEBUG'] ? Logger.new(STDOUT) : ScoutApm::Utils::NullLogger.new
106
+ private
107
+
108
+ def load_file(file)
109
+ if !File.exist?(@resolved_file_path)
110
+ logger.info("Configuration file #{file} does not exist, skipping.")
111
+ @file_loaded = false
112
+ return
113
+ end
114
+
115
+ if !app_environment
116
+ logger.info("Could not determine application environment, aborting configuration file load")
117
+ @file_loaded = false
118
+ return
119
+ end
120
+
121
+ begin
122
+ raw_file = File.read(@resolved_file_path)
123
+ erb_file = ERB.new(raw_file).result(binding)
124
+ parsed_yaml = YAML.load(erb_file)
125
+ @settings = parsed_yaml[app_environment]
126
+
127
+ if !@settings.is_a? Hash
128
+ raise ("Missing environment key for: #{app_environment}. This can happen if the key is missing, or with a malformed configuration file," +
129
+ " check that there is a top level #{app_environment} key.")
130
+ end
131
+
132
+ logger.info("Loaded Configuration: #{@resolved_file_path}. Using environment: #{app_environment}")
133
+ @file_loaded = true
134
+ rescue Exception => e # Explicit `Exception` handling to catch SyntaxError and anything else that ERB or YAML may throw
135
+ logger.info("Failed loading configuration file: #{e.message}. ScoutAPM will continue starting with configuration from ENV and defaults")
136
+ @file_loaded = false
137
+ end
138
+ end
139
+
140
+ def determine_file_path
141
+ File.join(ScoutApm::Environment.instance.root, "config", "scout_apm.yml")
142
+ end
143
+
144
+ def app_environment
145
+ ScoutApm::Environment.instance.env
146
+ end
147
+
148
+ # TODO: Make this better
149
+ def logger
150
+ ScoutApm::Agent.instance.logger || Logger.new(STDOUT)
107
151
  end
108
152
  end
109
153
  end
@@ -70,10 +70,6 @@ module ScoutApm
70
70
  framework_integration.database_engine
71
71
  end
72
72
 
73
- def raw_database_adapter
74
- framework_integration.raw_database_adapter
75
- end
76
-
77
73
  def processors
78
74
  @processors ||= begin
79
75
  proc_file = '/proc/cpuinfo'
@@ -88,12 +84,11 @@ module ScoutApm
88
84
  end
89
85
 
90
86
  def root
91
- return deploy_integration.root if deploy_integration
92
- framework_root
87
+ @root ||= deploy_integration? ? deploy_integration.root : framework_root
93
88
  end
94
89
 
95
90
  def framework_root
96
- if override_root = Agent.instance.config.value("application_root", true)
91
+ if override_root = Agent.instance.config.value("application_root")
97
92
  return override_root
98
93
  end
99
94
  if framework == :rails
@@ -108,8 +103,7 @@ module ScoutApm
108
103
  end
109
104
 
110
105
  def hostname
111
- config_hostname = Agent.instance.config.value("hostname", !Agent.instance.config.config_file_exists?)
112
- @hostname ||= config_hostname || platform_integration.hostname
106
+ @hostname ||= Agent.instance.config.value("hostname") || platform_integration.hostname
113
107
  end
114
108
 
115
109
  # Returns the whole integration object
@@ -134,7 +128,7 @@ module ScoutApm
134
128
  end
135
129
 
136
130
  def background_job_integration
137
- if Agent.instance.config.value("enable_background_jobs", !Agent.instance.config.config_file_exists?)
131
+ if Agent.instance.config.value("enable_background_jobs")
138
132
  @background_job_integration ||= BACKGROUND_JOB_INTEGRATIONS.detect {|integration| integration.present?}
139
133
  else
140
134
  nil
@@ -175,6 +169,18 @@ module ScoutApm
175
169
  @ruby_2 ||= defined?(RUBY_VERSION) && RUBY_VERSION.match(/^2/)
176
170
  end
177
171
 
172
+ # Returns a string representation of the OS (ex: darwin, linux)
173
+ def os
174
+ return @os if @os
175
+ raw_os = RbConfig::CONFIG['target_os']
176
+ match = raw_os.match(/([a-z]+)/)
177
+ if match
178
+ @os = match[1]
179
+ else
180
+ @os = raw_os
181
+ end
182
+ end
183
+
178
184
  ### framework checks
179
185
 
180
186
  def sinatra?
@@ -37,14 +37,18 @@ module ScoutApm
37
37
  default = :mysql
38
38
 
39
39
  if defined?(ActiveRecord::Base)
40
- case raw_database_adapter
41
- when "postgres" then :postgres
42
- when "postgresql" then :postgres
43
- when "postgis" then :postgres
44
- when "sqlite3" then :sqlite
45
- when "mysql" then :mysql
46
- when "mysql2" then :mysql
47
- else default
40
+ config = ActiveRecord::Base.configurations[env]
41
+ if config && config["adapter"]
42
+ case config["adapter"].to_s
43
+ when "postgres" then :postgres
44
+ when "postgresql" then :postgres
45
+ when "postgis" then :postgres
46
+ when "sqlite3" then :sqlite
47
+ when "mysql" then :mysql
48
+ else default
49
+ end
50
+ else
51
+ default
48
52
  end
49
53
  else
50
54
  default
@@ -52,12 +56,6 @@ module ScoutApm
52
56
  rescue
53
57
  default
54
58
  end
55
-
56
- def raw_database_adapter
57
- ActiveRecord::Base.configurations[env]["adapter"]
58
- rescue
59
- nil
60
- end
61
59
  end
62
60
  end
63
61
  end
@@ -34,19 +34,17 @@ module ScoutApm
34
34
 
35
35
  def database_engine
36
36
  return @database_engine if @database_engine
37
- default = :postgres
37
+ default = :mysql
38
38
 
39
39
  @database_engine = if defined?(ActiveRecord::Base)
40
- adapter = raw_database_adapter # can be nil
40
+ adapter = get_database_adapter # can be nil
41
41
 
42
42
  case adapter.to_s
43
43
  when "postgres" then :postgres
44
44
  when "postgresql" then :postgres
45
45
  when "postgis" then :postgres
46
46
  when "sqlite3" then :sqlite
47
- when "sqlite" then :sqlite
48
47
  when "mysql" then :mysql
49
- when "mysql2" then :mysql
50
48
  else default
51
49
  end
52
50
  else
@@ -55,21 +53,11 @@ module ScoutApm
55
53
  end
56
54
  end
57
55
 
58
- def raw_database_adapter
59
- adapter = if ActiveRecord::Base.respond_to?(:connection_config)
60
- ActiveRecord::Base.connection_config[:adapter].to_s
61
- else
62
- nil
63
- end
64
-
65
- if adapter.nil?
66
- adapter = ActiveRecord::Base.configurations[env]["adapter"]
67
- end
68
-
69
- return adapter
56
+ def get_database_adapter
57
+ ActiveRecord::Base.configurations[env]["adapter"]
70
58
  rescue # this would throw an exception if ActiveRecord::Base is defined but no configuration exists.
71
59
  nil
72
60
  end
73
61
  end
74
62
  end
75
- end
63
+ end
@@ -30,10 +30,6 @@ module ScoutApm
30
30
  def database_engine
31
31
  :mysql
32
32
  end
33
-
34
- def raw_database_adapter
35
- :mysql
36
- end
37
33
  end
38
34
  end
39
35
  end
@@ -37,10 +37,6 @@ module ScoutApm
37
37
  def database_engine
38
38
  :mysql
39
39
  end
40
-
41
- def raw_database_adapter
42
- :mysql
43
- end
44
40
  end
45
41
  end
46
42
  end
@@ -56,26 +56,6 @@ module ScoutApm
56
56
  end
57
57
  end
58
58
 
59
- # Given a value, where in this histogram does it fall?
60
- # Returns a float between 0 and 1
61
- def approximate_quantile_of_value(v)
62
- mutex.synchronize do
63
- return 100 if total == 0
64
-
65
- count_examined = 0
66
-
67
- bins.each_with_index do |bin, index|
68
- if v <= bin.value
69
- break
70
- end
71
-
72
- count_examined += bin.count
73
- end
74
-
75
- count_examined / total.to_f
76
- end
77
- end
78
-
79
59
  def mean
80
60
  mutex.synchronize do
81
61
  if total == 0
@@ -61,10 +61,7 @@ module ScoutApm
61
61
  req = ScoutApm::RequestManager.lookup
62
62
  path = ScoutApm::Agent.instance.config.value("uri_reporting") == 'path' ? request.path : request.fullpath
63
63
  req.annotate_request(:uri => path)
64
-
65
- # IP Spoofing Protection can throw an exception, just move on w/o remote ip
66
- req.context.add_user(:ip => request.remote_ip) rescue nil
67
-
64
+ req.context.add_user(:ip => request.remote_ip)
68
65
  req.set_headers(request.headers)
69
66
  req.web!
70
67
 
@@ -27,14 +27,44 @@ module ScoutApm
27
27
  end
28
28
 
29
29
  def add_instruments
30
- if defined?(::ActiveRecord) && defined?(::ActiveRecord::Base)
30
+ # Setup Tracer on AR::Base
31
+ if Utils::KlassHelper.defined?("ActiveRecord::Base")
32
+ ::ActiveRecord::Base.class_eval do
33
+ include ::ScoutApm::Tracer
34
+ end
35
+ end
36
+
37
+ # Install #log tracing
38
+ if Utils::KlassHelper.defined?("ActiveRecord::ConnectionAdapters::AbstractAdapter")
31
39
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.module_eval do
32
40
  include ::ScoutApm::Instruments::ActiveRecordInstruments
33
41
  include ::ScoutApm::Tracer
34
42
  end
43
+ end
44
+
45
+ if Utils::KlassHelper.defined?("ActiveRecord::Querying")
46
+ ::ActiveRecord::Querying.module_eval do
47
+ include ::ScoutApm::Tracer
48
+ include ::ScoutApm::Instruments::ActiveRecordQueryingInstruments
49
+ end
50
+ end
35
51
 
36
- ::ActiveRecord::Base.class_eval do
52
+ if Utils::KlassHelper.defined?("ActiveRecord::FinderMethods")
53
+ ::ActiveRecord::FinderMethods.module_eval do
37
54
  include ::ScoutApm::Tracer
55
+ include ::ScoutApm::Instruments::ActiveRecordFinderMethodsInstruments
56
+ end
57
+ end
58
+
59
+ if Utils::KlassHelper.defined?("ActiveSupport::Notifications")
60
+ ActiveSupport::Notifications.subscribe("instantiation.active_record") do |event_name, start, stop, uuid, payload|
61
+ req = ScoutApm::RequestManager.lookup
62
+ layer = req.current_layer
63
+ if layer && layer.type == "ActiveRecord"
64
+ layer.annotate_layer(payload)
65
+ else
66
+ ScoutApm::Agent.instance.logger.debug("Expected layer type: ActiveRecord, got #{layer && layer.type}")
67
+ end
38
68
  end
39
69
  end
40
70
  rescue
@@ -44,6 +74,13 @@ module ScoutApm
44
74
 
45
75
  # Contains ActiveRecord instrument, aliasing +ActiveRecord::ConnectionAdapters::AbstractAdapter#log+ calls
46
76
  # to trace calls to the database.
77
+ ################################################################################
78
+ # #log instrument.
79
+ #
80
+ # #log is very close to where AR calls out to the database itself. We have access
81
+ # to the real SQL, and an AR generated "name" for the Query
82
+ #
83
+ ################################################################################
47
84
  module ActiveRecordInstruments
48
85
  def self.included(instrumented_class)
49
86
  ScoutApm::Agent.instance.logger.info "Instrumenting #{instrumented_class.inspect}"
@@ -51,19 +88,123 @@ module ScoutApm
51
88
  unless instrumented_class.method_defined?(:log_without_scout_instruments)
52
89
  alias_method :log_without_scout_instruments, :log
53
90
  alias_method :log, :log_with_scout_instruments
54
- protected :log
55
91
  end
56
92
  end
57
- end # self.included
93
+ end
58
94
 
59
95
  def log_with_scout_instruments(*args, &block)
96
+ # Extract data from the arguments
60
97
  sql, name = args
61
- self.class.instrument("ActiveRecord",
62
- Utils::ActiveRecordMetricName.new(sql, name),
63
- :desc => Utils::SqlSanitizer.new(sql) ) do
98
+ metric_name = Utils::ActiveRecordMetricName.new(sql, name)
99
+ desc = Utils::SqlSanitizer.new(sql)
100
+
101
+ # Get current ScoutApm context
102
+ req = ScoutApm::RequestManager.lookup
103
+ current_layer = req.current_layer
104
+
105
+
106
+ # If we call #log, we have a real query to run, and we've already
107
+ # gotten through the cache gatekeeper. Since we want to only trace real
108
+ # queries, and not repeated identical queries that just hit cache, we
109
+ # mark layer as ignorable initially in #find_by_sql, then only when we
110
+ # know it's a real database call do we mark it back as usable.
111
+ #
112
+ # This flag is later used in SlowRequestConverter to skip adding ignorable layers
113
+ current_layer.annotate_layer(:ignorable => false) if current_layer
114
+
115
+ # Either: update the current layer and yield, don't start a new one.
116
+ if current_layer && current_layer.type == "ActiveRecord"
117
+ # TODO: Get rid of call .to_s, need to find this without forcing a previous run of the name logic
118
+ if current_layer.name.to_s == Utils::ActiveRecordMetricName::DEFAULT_METRIC
119
+ current_layer.name = metric_name
120
+ current_layer.desc = desc
121
+ end
122
+
64
123
  log_without_scout_instruments(sql, name, &block)
124
+
125
+ # OR: Start a new layer, we didn't pick up instrumentation earlier in the stack.
126
+ else
127
+ layer = ScoutApm::Layer.new("ActiveRecord", metric_name)
128
+ layer.desc = desc
129
+ req.start_layer(layer)
130
+ begin
131
+ log_without_scout_instruments(sql, name, &block)
132
+ ensure
133
+ req.stop_layer
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ ################################################################################
140
+ # Entry-point of instruments.
141
+ #
142
+ # We instrument both ActiveRecord::Querying#find_by_sql and
143
+ # ActiveRecord::FinderMethods#find_with_associations. These are early in
144
+ # the chain of calls when you're using ActiveRecord.
145
+ #
146
+ # Later on, they will call into #log, which we also instrument, at which
147
+ # point, we can fill in additional data gathered at that point (name, sql)
148
+ #
149
+ # Caveats:
150
+ # * We don't have a name for the query yet.
151
+ # * The query hasn't hit the cache yet. In the case of a cache hit, we
152
+ # won't hit #log, so won't get a name, leaving the misleading default.
153
+ # * One call here can result in several calls to #log, especially in the
154
+ # case where Rails needs to load the schema details for the table being
155
+ # queried.
156
+ ################################################################################
157
+
158
+ module ActiveRecordQueryingInstruments
159
+ def self.included(instrumented_class)
160
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActiveRecord::Querying - #{instrumented_class.inspect}"
161
+ instrumented_class.class_eval do
162
+ unless instrumented_class.method_defined?(:find_by_sql_without_scout_instruments)
163
+ alias_method :find_by_sql_without_scout_instruments, :find_by_sql
164
+ alias_method :find_by_sql, :find_by_sql_with_scout_instruments
165
+ end
166
+ end
167
+ end
168
+
169
+ def find_by_sql_with_scout_instruments(*args, &block)
170
+ req = ScoutApm::RequestManager.lookup
171
+ layer = ScoutApm::Layer.new("ActiveRecord", Utils::ActiveRecordMetricName::DEFAULT_METRIC)
172
+ layer.annotate_layer(:ignorable => true)
173
+ req.start_layer(layer)
174
+ req.ignore_children!
175
+ begin
176
+ find_by_sql_without_scout_instruments(*args)
177
+ ensure
178
+ req.acknowledge_children!
179
+ req.stop_layer
65
180
  end
66
181
  end
67
- end # module ActiveRecordInstruments
182
+ end
183
+
184
+ module ActiveRecordFinderMethodsInstruments
185
+ def self.included(instrumented_class)
186
+ ScoutApm::Agent.instance.logger.info "Instrumenting ActiveRecord::FinderMethods - #{instrumented_class.inspect}"
187
+ instrumented_class.class_eval do
188
+ unless instrumented_class.method_defined?(:find_with_associations_without_scout_instruments)
189
+ alias_method :find_with_associations_without_scout_instruments, :find_with_associations
190
+ alias_method :find_with_associations, :find_with_associations_with_scout_instruments
191
+ end
192
+ end
193
+ end
194
+
195
+ def find_with_associations_with_scout_instruments(*args, &block)
196
+ req = ScoutApm::RequestManager.lookup
197
+ layer = ScoutApm::Layer.new("ActiveRecord", Utils::ActiveRecordMetricName::DEFAULT_METRIC)
198
+ layer.annotate_layer(:ignorable => true)
199
+ req.start_layer(layer)
200
+ req.ignore_children!
201
+ begin
202
+ find_with_associations_without_scout_instruments(*args)
203
+ ensure
204
+ req.acknowledge_children!
205
+ req.stop_layer
206
+ end
207
+ end
208
+ end
68
209
  end
69
210
  end