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.
- checksums.yaml +4 -4
- data/.gitignore +8 -1
- data/CHANGELOG.markdown +7 -57
- data/ext/allocations/allocations.c +84 -0
- data/ext/allocations/extconf.rb +3 -0
- data/lib/scout_apm/agent/reporting.rb +9 -32
- data/lib/scout_apm/agent.rb +45 -31
- data/lib/scout_apm/app_server_load.rb +1 -2
- data/lib/scout_apm/attribute_arranger.rb +0 -4
- data/lib/scout_apm/background_worker.rb +6 -9
- data/lib/scout_apm/bucket_name_splitter.rb +3 -3
- data/lib/scout_apm/call_set.rb +1 -0
- data/lib/scout_apm/config.rb +110 -66
- data/lib/scout_apm/environment.rb +16 -10
- data/lib/scout_apm/framework_integrations/rails_2.rb +12 -14
- data/lib/scout_apm/framework_integrations/rails_3_or_4.rb +5 -17
- data/lib/scout_apm/framework_integrations/ruby.rb +0 -4
- data/lib/scout_apm/framework_integrations/sinatra.rb +0 -4
- data/lib/scout_apm/histogram.rb +0 -20
- data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +1 -4
- data/lib/scout_apm/instruments/active_record.rb +149 -8
- data/lib/scout_apm/instruments/mongoid.rb +5 -78
- data/lib/scout_apm/instruments/process/process_cpu.rb +0 -12
- data/lib/scout_apm/instruments/process/process_memory.rb +14 -43
- data/lib/scout_apm/layaway.rb +34 -134
- data/lib/scout_apm/layaway_file.rb +50 -27
- data/lib/scout_apm/layer.rb +45 -1
- data/lib/scout_apm/layer_converters/allocation_metric_converter.rb +17 -0
- data/lib/scout_apm/layer_converters/converter_base.rb +4 -6
- data/lib/scout_apm/layer_converters/job_converter.rb +1 -0
- data/lib/scout_apm/layer_converters/metric_converter.rb +2 -1
- data/lib/scout_apm/layer_converters/slow_job_converter.rb +42 -21
- data/lib/scout_apm/layer_converters/slow_request_converter.rb +58 -37
- data/lib/scout_apm/metric_meta.rb +1 -5
- data/lib/scout_apm/metric_set.rb +6 -15
- data/lib/scout_apm/reporter.rb +4 -6
- data/lib/scout_apm/serializers/metrics_to_json_serializer.rb +5 -1
- data/lib/scout_apm/serializers/payload_serializer_to_json.rb +1 -3
- data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +5 -3
- data/lib/scout_apm/slow_job_policy.rb +19 -89
- data/lib/scout_apm/slow_job_record.rb +12 -20
- data/lib/scout_apm/slow_request_policy.rb +12 -80
- data/lib/scout_apm/slow_transaction.rb +16 -20
- data/lib/scout_apm/stackprof_tree_collapser.rb +103 -0
- data/lib/scout_apm/store.rb +16 -78
- data/lib/scout_apm/tracked_request.rb +53 -36
- data/lib/scout_apm/utils/active_record_metric_name.rb +2 -0
- data/lib/scout_apm/utils/fake_stack_prof.rb +40 -0
- data/lib/scout_apm/utils/klass_helper.rb +26 -0
- data/lib/scout_apm/utils/sql_sanitizer.rb +1 -1
- data/lib/scout_apm/utils/sql_sanitizer_regex.rb +2 -2
- data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +2 -2
- data/lib/scout_apm/version.rb +1 -1
- data/lib/scout_apm.rb +13 -7
- data/scout_apm.gemspec +3 -1
- data/test/test_helper.rb +3 -4
- data/test/unit/layaway_test.rb +8 -5
- data/test/unit/serializers/payload_serializer_test.rb +2 -2
- data/test/unit/slow_item_set_test.rb +1 -2
- data/test/unit/sql_sanitizer_test.rb +0 -6
- metadata +28 -20
- data/LICENSE.md +0 -27
- data/lib/scout_apm/instruments/grape.rb +0 -69
- data/lib/scout_apm/instruments/percentile_sampler.rb +0 -37
- data/lib/scout_apm/request_histograms.rb +0 -46
- data/lib/scout_apm/scored_item_set.rb +0 -79
- data/test/unit/metric_set_test.rb +0 -101
- data/test/unit/scored_item_set_test.rb +0 -65
- data/test/unit/slow_request_policy_test.rb +0 -42
data/lib/scout_apm/config.rb
CHANGED
@@ -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
|
-
#
|
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'.
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
64
|
-
@config_path || File.join(ScoutApm::Environment.instance.root, "config", "scout_apm.yml")
|
62
|
+
nil
|
65
63
|
end
|
66
64
|
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
72
|
-
|
76
|
+
def value(key)
|
77
|
+
DEFAULTS[key]
|
78
|
+
end
|
73
79
|
end
|
74
80
|
|
75
|
-
|
76
|
-
(
|
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
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
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"
|
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
|
-
|
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"
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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 = :
|
37
|
+
default = :mysql
|
38
38
|
|
39
39
|
@database_engine = if defined?(ActiveRecord::Base)
|
40
|
-
adapter =
|
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
|
59
|
-
|
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
|
data/lib/scout_apm/histogram.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
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
|