scout_apm 0.1.3.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +5 -0
- data/Rakefile +7 -0
- data/lib/scout_apm/agent.rb +17 -22
- data/lib/scout_apm/agent/logging.rb +12 -5
- data/lib/scout_apm/agent/reporting.rb +4 -4
- data/lib/scout_apm/config.rb +41 -21
- data/lib/scout_apm/environment.rb +47 -22
- data/lib/scout_apm/instruments/active_record_instruments.rb +48 -59
- data/lib/scout_apm/utils/null_logger.rb +13 -0
- data/lib/scout_apm/utils/sql_sanitizer.rb +74 -0
- data/lib/scout_apm/version.rb +1 -1
- data/scout_apm.gemspec +4 -4
- data/test/data/config_test_1.yml +21 -0
- data/test/test_helper.rb +16 -0
- data/test/unit/config_test.rb +29 -0
- data/test/unit/environment_test.rb +60 -0
- data/test/unit/instruments/active_record_instruments_test.rb +7 -0
- data/test/unit/sql_sanitizer_test.rb +61 -0
- metadata +61 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eadc6cf693f387c94fdbbaa5bc96a759c80be140
|
4
|
+
data.tar.gz: e6c9465cde1d1589f72e6c46f95a9e45ba54f627
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77a3ee7b7e4fd8b1c3ee62231d4566d1bf9937ea4d48498194b991d7ffb55782c13baaae1021aa127325dccc9ccd2a200722b5b5ae7a341825a2acf45f5c854b
|
7
|
+
data.tar.gz: 2aa25dca40e455d4463676c77f6bd105d07402c6358d9e479ba6033b143464e8d546ff39a4ea50e82f0b9e9c92959bb393f69ba9cec3d50ba3d35c67268591a9
|
data/CHANGELOG.markdown
CHANGED
data/Rakefile
CHANGED
data/lib/scout_apm/agent.rb
CHANGED
@@ -6,8 +6,8 @@ module ScoutApm
|
|
6
6
|
# saves tshe merged data to disk, and sends it to the Scout server.
|
7
7
|
class Agent
|
8
8
|
# see self.instance
|
9
|
-
@@instance = nil
|
10
|
-
|
9
|
+
@@instance = nil
|
10
|
+
|
11
11
|
# Accessors below are for associated classes
|
12
12
|
attr_accessor :store
|
13
13
|
attr_accessor :layaway
|
@@ -18,31 +18,32 @@ module ScoutApm
|
|
18
18
|
attr_accessor :log_file # path to the log file
|
19
19
|
attr_accessor :options # options passed to the agent when +#start+ is called.
|
20
20
|
attr_accessor :metric_lookup # Hash used to lookup metric ids based on their name and scope
|
21
|
-
|
21
|
+
|
22
22
|
# All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.
|
23
23
|
def self.instance(options = {})
|
24
24
|
@@instance ||= self.new(options)
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
# Note - this doesn't start instruments or the worker thread. This is handled via +#start+ as we don't
|
28
28
|
# want to start the worker thread or install instrumentation if (1) disabled for this environment (2) a worker thread shouldn't
|
29
29
|
# be started (when forking).
|
30
30
|
def initialize(options = {})
|
31
31
|
@started = false
|
32
32
|
@options ||= options
|
33
|
+
@config = ScoutApm::Config.new(options[:config_path])
|
34
|
+
|
33
35
|
@store = ScoutApm::Store.new
|
34
36
|
@layaway = ScoutApm::Layaway.new
|
35
|
-
@config = ScoutApm::Config.new(options[:config_path])
|
36
37
|
@metric_lookup = Hash.new
|
37
|
-
@process_cpu=ScoutApm::Instruments::Process::ProcessCpu.new(environment.processors)
|
38
|
-
@process_memory=ScoutApm::Instruments::Process::ProcessMemory.new
|
38
|
+
@process_cpu = ScoutApm::Instruments::Process::ProcessCpu.new(environment.processors)
|
39
|
+
@process_memory = ScoutApm::Instruments::Process::ProcessMemory.new
|
39
40
|
@capacity = ScoutApm::Capacity.new
|
40
41
|
end
|
41
|
-
|
42
|
+
|
42
43
|
def environment
|
43
44
|
@environment ||= ScoutApm::Environment.new
|
44
45
|
end
|
45
|
-
|
46
|
+
|
46
47
|
# This is called via +ScoutApm::Agent.instance.start+ when ScoutApm is required in a Ruby application.
|
47
48
|
# It initializes the agent and starts the worker thread (if appropiate).
|
48
49
|
def start(options = {})
|
@@ -64,7 +65,7 @@ module ScoutApm
|
|
64
65
|
end
|
65
66
|
@started = true
|
66
67
|
logger.info "Starting monitoring for [#{config.value('name')}]. Framework [#{environment.framework}] App Server [#{environment.app_server}]."
|
67
|
-
|
68
|
+
load_instruments
|
68
69
|
if !start_background_worker?
|
69
70
|
logger.debug "Not starting worker thread. Will start worker loops after forking."
|
70
71
|
install_passenger_events if environment.app_server == :passenger
|
@@ -168,8 +169,8 @@ module ScoutApm
|
|
168
169
|
end
|
169
170
|
rescue
|
170
171
|
logger.warn "Unable to install Puma worker loop: #{$!.message}"
|
171
|
-
end
|
172
|
-
|
172
|
+
end
|
173
|
+
|
173
174
|
# Creates the worker thread. The worker thread is a loop that runs continuously. It sleeps for +Agent#period+ and when it wakes,
|
174
175
|
# processes data, either saving it to disk or reporting to Scout.
|
175
176
|
def start_background_worker
|
@@ -180,7 +181,7 @@ module ScoutApm
|
|
180
181
|
end # thread new
|
181
182
|
logger.debug "Done creating worker thread."
|
182
183
|
end
|
183
|
-
|
184
|
+
|
184
185
|
# Called from #process_metrics, which is run via the background worker.
|
185
186
|
def run_samplers
|
186
187
|
begin
|
@@ -203,9 +204,10 @@ module ScoutApm
|
|
203
204
|
logger.debug e.backtrace.join("\n")
|
204
205
|
end
|
205
206
|
end
|
206
|
-
|
207
|
+
|
207
208
|
# Loads the instrumention logic.
|
208
209
|
def load_instruments
|
210
|
+
logger.debug "Installing instrumentation"
|
209
211
|
case environment.framework
|
210
212
|
when :rails
|
211
213
|
require File.expand_path(File.join(File.dirname(__FILE__),'instruments/rails/action_controller_instruments.rb'))
|
@@ -221,12 +223,5 @@ module ScoutApm
|
|
221
223
|
logger.warn $!.message
|
222
224
|
logger.warn $!.backtrace
|
223
225
|
end
|
224
|
-
|
225
|
-
# Injects instruments into the Ruby application.
|
226
|
-
def start_instruments
|
227
|
-
logger.debug "Installing instrumentation"
|
228
|
-
load_instruments
|
229
|
-
end
|
230
|
-
|
231
226
|
end # class Agent
|
232
|
-
end # module ScoutApm
|
227
|
+
end # module ScoutApm
|
@@ -6,11 +6,18 @@ module ScoutApm
|
|
6
6
|
def default_log_path
|
7
7
|
"#{environment.root}/log"
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def init_logger
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
begin
|
12
|
+
@log_file = wants_stdout? ? STDOUT : "#{log_file_path}/scout_apm.log"
|
13
|
+
rescue => e
|
14
|
+
puts "OHH NO"
|
15
|
+
puts e.message
|
16
|
+
puts e.backtrace.join("\n\t")
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
@logger = Logger.new(@log_file)
|
14
21
|
@logger.level = log_level
|
15
22
|
apply_log_format
|
16
23
|
rescue Exception => e
|
@@ -50,4 +57,4 @@ module ScoutApm
|
|
50
57
|
end # module Logging
|
51
58
|
include Logging
|
52
59
|
end # class Agent
|
53
|
-
end # moudle ScoutApm
|
60
|
+
end # moudle ScoutApm
|
@@ -23,7 +23,7 @@ module ScoutApm
|
|
23
23
|
if meta.metric_name =~ /\AController/
|
24
24
|
controller_count += stats.call_count
|
25
25
|
end
|
26
|
-
end
|
26
|
+
end
|
27
27
|
payload = Marshal.dump(:metrics => metrics, :slow_transactions => slow_transactions)
|
28
28
|
slow_transactions_kb = Marshal.dump(slow_transactions).size/1024 # just for performance debugging
|
29
29
|
logger.debug "#{config.value('name')} Delivering total payload [#{payload.size/1024} KB] for #{controller_count} requests and slow transactions [#{slow_transactions_kb} KB] for #{slow_transactions.size} transactions of durations: #{slow_transactions.map(&:total_call_time).join(',')}."
|
@@ -47,7 +47,7 @@ module ScoutApm
|
|
47
47
|
logger.info $!.message
|
48
48
|
logger.debug $!.backtrace
|
49
49
|
end
|
50
|
-
|
50
|
+
|
51
51
|
# Before reporting, lookup metric_id for each MetricMeta. This speeds up
|
52
52
|
# reporting on the server-side.
|
53
53
|
def add_metric_ids(metrics)
|
@@ -57,7 +57,7 @@ module ScoutApm
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
61
|
def checkin_uri
|
62
62
|
URI.parse("#{config.value('host')}/apps/checkin.scout?key=#{config.value('key')}&name=#{CGI.escape(config.value('name'))}")
|
63
63
|
end
|
@@ -113,4 +113,4 @@ module ScoutApm
|
|
113
113
|
end # module Reporting
|
114
114
|
include Reporting
|
115
115
|
end # class Agent
|
116
|
-
end # module ScoutApm
|
116
|
+
end # module ScoutApm
|
data/lib/scout_apm/config.rb
CHANGED
@@ -1,15 +1,22 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
require 'scout_apm/environment'
|
5
|
+
|
1
6
|
module ScoutApm
|
2
|
-
class Config
|
7
|
+
class Config
|
3
8
|
DEFAULTS = {
|
4
9
|
'host' => 'https://apm.scoutapp.com',
|
5
10
|
'log_level' => 'info'
|
6
|
-
}
|
11
|
+
}.freeze
|
7
12
|
|
8
13
|
def initialize(config_path = nil)
|
9
14
|
@config_path = config_path
|
10
15
|
end
|
11
|
-
|
12
|
-
# Fetch a config value.
|
16
|
+
|
17
|
+
# Fetch a config value.
|
18
|
+
# It first attempts to fetch an ENV var prefixed with 'SCOUT_',
|
19
|
+
# then from the settings file.
|
13
20
|
def value(key)
|
14
21
|
value = ENV['SCOUT_'+key.upcase] || settings[key]
|
15
22
|
value.to_s.strip.length.zero? ? nil : value
|
@@ -18,33 +25,46 @@ module ScoutApm
|
|
18
25
|
private
|
19
26
|
|
20
27
|
def config_path
|
21
|
-
@config_path || File.join(ScoutApm::
|
28
|
+
@config_path || File.join(ScoutApm::Environment.new.root, "config", "scout_apm.yml")
|
22
29
|
end
|
23
|
-
|
30
|
+
|
24
31
|
def config_file
|
25
32
|
File.expand_path(config_path)
|
26
33
|
end
|
27
34
|
|
28
35
|
def settings
|
29
|
-
|
30
|
-
|
36
|
+
@settings ||= load_file
|
37
|
+
end
|
38
|
+
|
39
|
+
def config_environment
|
40
|
+
@config_environment ||= ScoutApm::Environment.new.env
|
31
41
|
end
|
32
|
-
|
42
|
+
|
33
43
|
def load_file
|
44
|
+
settings_hash = {}
|
34
45
|
begin
|
35
|
-
if
|
36
|
-
|
37
|
-
@settings = {}
|
46
|
+
if File.exist?(config_file)
|
47
|
+
settings_hash = YAML.load(ERB.new(File.read(config_file)).result(binding))[config_environment] || {}
|
38
48
|
else
|
39
|
-
|
40
|
-
end
|
49
|
+
logger.warn "No config file found at [#{config_file}]."
|
50
|
+
end
|
41
51
|
rescue Exception => e
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
52
|
+
logger.warn "Unable to load the config file."
|
53
|
+
logger.warn e.message
|
54
|
+
logger.warn e.backtrace
|
55
|
+
end
|
56
|
+
DEFAULTS.merge(settings_hash)
|
57
|
+
end
|
58
|
+
|
59
|
+
# if we error out early enough, we don't have access to ScoutApm's logger
|
60
|
+
# in that case, be silent unless ENV['SCOUT_DEBUG'] is set, then STDOUT it
|
61
|
+
def logger
|
62
|
+
if defined?(ScoutApm::Agent) && (apm_log = ScoutApm::Agent.instance.logger)
|
63
|
+
apm_log
|
64
|
+
else
|
65
|
+
require 'scout_apm/utils/null_logger'
|
66
|
+
ENV['SCOUT_DEBUG'] ? Logger.new(STDOUT) : ScoutApm::Utils::NullLogger.new
|
46
67
|
end
|
47
|
-
@settings = DEFAULTS.merge(@settings)
|
48
68
|
end
|
49
|
-
end
|
50
|
-
end
|
69
|
+
end
|
70
|
+
end
|
@@ -3,13 +3,17 @@ module ScoutApm
|
|
3
3
|
class Environment
|
4
4
|
def env
|
5
5
|
@env ||= case framework
|
6
|
-
when :rails
|
7
|
-
|
6
|
+
when :rails
|
7
|
+
RAILS_ENV.dup
|
8
|
+
when :rails3_or_4
|
9
|
+
Rails.env
|
8
10
|
when :sinatra
|
9
11
|
ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
12
|
+
when :ruby
|
13
|
+
ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
|
10
14
|
end
|
11
15
|
end
|
12
|
-
|
16
|
+
|
13
17
|
def framework
|
14
18
|
@framework ||= case
|
15
19
|
when defined?(::Rails) && defined?(ActionController)
|
@@ -22,7 +26,29 @@ module ScoutApm
|
|
22
26
|
else :ruby
|
23
27
|
end
|
24
28
|
end
|
25
|
-
|
29
|
+
|
30
|
+
def database_engine
|
31
|
+
default = :mysql
|
32
|
+
|
33
|
+
if defined?(ActiveRecord::Base)
|
34
|
+
return default unless ActiveRecord::Base.connected?
|
35
|
+
|
36
|
+
case ActiveRecord::Base.connection.class.to_s
|
37
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
|
38
|
+
:mysql
|
39
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
40
|
+
:postgres
|
41
|
+
when "ActiveRecord::ConnectionAdapters::SQLite3Adapter"
|
42
|
+
:sqlite
|
43
|
+
else
|
44
|
+
default
|
45
|
+
end
|
46
|
+
else
|
47
|
+
# TODO: detection outside of Rails
|
48
|
+
default
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
26
52
|
def processors
|
27
53
|
return @processors if @processors
|
28
54
|
unless @processors
|
@@ -38,7 +64,7 @@ module ScoutApm
|
|
38
64
|
end
|
39
65
|
@processors
|
40
66
|
end
|
41
|
-
|
67
|
+
|
42
68
|
def root
|
43
69
|
if framework == :rails
|
44
70
|
RAILS_ROOT.to_s
|
@@ -52,23 +78,23 @@ module ScoutApm
|
|
52
78
|
end
|
53
79
|
|
54
80
|
def heroku?
|
55
|
-
ENV['DYNO']
|
81
|
+
ENV['DYNO']
|
56
82
|
end
|
57
83
|
|
58
84
|
def hostname
|
59
85
|
heroku? ? ENV['DYNO'] : Socket.gethostname
|
60
86
|
end
|
61
|
-
|
87
|
+
|
62
88
|
# This needs to be improved. Frequently, multiple app servers gem are present and which
|
63
|
-
# ever is checked first becomes the designated app server.
|
64
|
-
#
|
89
|
+
# ever is checked first becomes the designated app server.
|
90
|
+
#
|
65
91
|
# I've put Thin and Webrick last as they are often used in development and included in Gemfiles
|
66
92
|
# but less likely used in production.
|
67
93
|
#
|
68
94
|
# Next step: (1) list out all detected app servers (2) install hooks for those that need it (passenger, rainbows, unicorn).
|
69
95
|
#
|
70
96
|
# Believe the biggest downside is the master process for forking app servers will get a background worker. Not sure how this will
|
71
|
-
# impact metrics (it shouldn't process requests).
|
97
|
+
# impact metrics (it shouldn't process requests).
|
72
98
|
def app_server
|
73
99
|
@app_server ||= if passenger? then :passenger
|
74
100
|
elsif rainbows? then :rainbows
|
@@ -79,9 +105,8 @@ module ScoutApm
|
|
79
105
|
else nil
|
80
106
|
end
|
81
107
|
end
|
82
|
-
|
108
|
+
|
83
109
|
### app server related-checks
|
84
|
-
|
85
110
|
def thin?
|
86
111
|
if defined?(::Thin) && defined?(::Thin::Server)
|
87
112
|
# Ensure Thin is actually initialized. It could just be required and not running.
|
@@ -89,14 +114,14 @@ module ScoutApm
|
|
89
114
|
false
|
90
115
|
end
|
91
116
|
end
|
92
|
-
|
117
|
+
|
93
118
|
# Called via +#forking?+ since Passenger forks. Adds an event listener to start the worker thread
|
94
119
|
# inside the passenger worker process.
|
95
120
|
# Background: http://www.modrails.com/documentation/Users%20guide%20Nginx.html#spawning%5Fmethods%5Fexplained
|
96
121
|
def passenger?
|
97
122
|
(defined?(::Passenger) && defined?(::Passenger::AbstractServer)) || defined?(::PhusionPassenger)
|
98
123
|
end
|
99
|
-
|
124
|
+
|
100
125
|
def webrick?
|
101
126
|
defined?(::WEBrick) && defined?(::WEBrick::VERSION)
|
102
127
|
end
|
@@ -107,7 +132,7 @@ module ScoutApm
|
|
107
132
|
false
|
108
133
|
end
|
109
134
|
end
|
110
|
-
|
135
|
+
|
111
136
|
def unicorn?
|
112
137
|
if defined?(::Unicorn) && defined?(::Unicorn::HttpServer)
|
113
138
|
# Ensure Unicorn is actually initialized. It could just be required and not running.
|
@@ -119,15 +144,15 @@ module ScoutApm
|
|
119
144
|
def puma?
|
120
145
|
defined?(::Puma) && File.basename($0) == 'puma'
|
121
146
|
end
|
122
|
-
|
147
|
+
|
123
148
|
# If forking, don't start worker thread in the master process. Since it's started as a Thread, it won't survive
|
124
|
-
# the fork.
|
149
|
+
# the fork.
|
125
150
|
def forking?
|
126
151
|
passenger? or unicorn? or rainbows? or puma?
|
127
152
|
end
|
128
|
-
|
153
|
+
|
129
154
|
### ruby checks
|
130
|
-
|
155
|
+
|
131
156
|
def rubinius?
|
132
157
|
RUBY_VERSION =~ /rubinius/i
|
133
158
|
end
|
@@ -135,11 +160,11 @@ module ScoutApm
|
|
135
160
|
def jruby?
|
136
161
|
defined?(JRuby)
|
137
162
|
end
|
138
|
-
|
163
|
+
|
139
164
|
def ruby_19?
|
140
165
|
defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby" && RUBY_VERSION.match(/^1\.9/)
|
141
166
|
end
|
142
|
-
|
167
|
+
|
143
168
|
### framework checks
|
144
169
|
|
145
170
|
def sinatra?
|
@@ -147,4 +172,4 @@ module ScoutApm
|
|
147
172
|
end
|
148
173
|
|
149
174
|
end # class Environemnt
|
150
|
-
end
|
175
|
+
end
|
@@ -1,65 +1,54 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
require 'scout_apm/utils/sql_sanitizer'
|
2
|
+
|
3
|
+
module ScoutApm
|
4
|
+
module Instruments
|
5
|
+
# Contains ActiveRecord instrument, aliasing +ActiveRecord::ConnectionAdapters::AbstractAdapter#log+ calls
|
6
|
+
# to trace calls to the database.
|
7
|
+
module ActiveRecordInstruments
|
8
|
+
def self.included(instrumented_class)
|
9
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting #{instrumented_class.inspect}"
|
10
|
+
instrumented_class.class_eval do
|
11
|
+
unless instrumented_class.method_defined?(:log_without_scout_instruments)
|
12
|
+
alias_method :log_without_scout_instruments, :log
|
13
|
+
alias_method :log, :log_with_scout_instruments
|
14
|
+
protected :log
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end # self.included
|
18
|
+
|
19
|
+
def log_with_scout_instruments(*args, &block)
|
20
|
+
sql, name = args
|
21
|
+
self.class.instrument(scout_ar_metric_name(sql,name), :desc => Utils::SqlSanitizer.new(sql).to_s) do
|
22
|
+
log_without_scout_instruments(sql, name, &block)
|
12
23
|
end
|
13
24
|
end
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
when 'load' then 'find'
|
31
|
-
when 'indexes', 'columns' then nil # not under developer control
|
32
|
-
when 'destroy', 'find', 'save', 'create', 'exists' then operation
|
33
|
-
when 'update' then 'save'
|
34
|
-
else
|
35
|
-
if model == 'Join'
|
36
|
-
operation
|
25
|
+
|
26
|
+
def scout_ar_metric_name(sql,name)
|
27
|
+
# sql: SELECT "places".* FROM "places" ORDER BY "places"."position" ASC
|
28
|
+
# name: Place Load
|
29
|
+
if name && (parts = name.split " ") && parts.size == 2
|
30
|
+
model = parts.first
|
31
|
+
operation = parts.last.downcase
|
32
|
+
metric_name = case operation
|
33
|
+
when 'load' then 'find'
|
34
|
+
when 'indexes', 'columns' then nil # not under developer control
|
35
|
+
when 'destroy', 'find', 'save', 'create', 'exists' then operation
|
36
|
+
when 'update' then 'save'
|
37
|
+
else
|
38
|
+
if model == 'Join'
|
39
|
+
operation
|
40
|
+
end
|
37
41
|
end
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
42
|
+
metric = "ActiveRecord/#{model}/#{metric_name}" if metric_name
|
43
|
+
metric = "ActiveRecord/SQL/other" if metric.nil?
|
44
|
+
else
|
45
|
+
metric = "ActiveRecord/SQL/Unknown"
|
46
|
+
end
|
47
|
+
metric
|
43
48
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
# Removes actual values from SQL. Used to both obfuscate the SQL and group
|
48
|
-
# similar queries in the UI.
|
49
|
-
def scout_sanitize_sql(sql)
|
50
|
-
return nil if sql.length > 1000 # safeguard - don't sanitize large SQL statements
|
51
|
-
sql = sql.dup
|
52
|
-
sql.gsub!(/\\"/, '') # removing escaping double quotes
|
53
|
-
sql.gsub!(/\\'/, '') # removing escaping single quotes
|
54
|
-
sql.gsub!(/'(?:[^']|'')*'/, '?') # removing strings (single quote)
|
55
|
-
sql.gsub!(/"(?:[^"]|"")*"/, '?') # removing strings (double quote)
|
56
|
-
sql.gsub!(/\b\d+\b/, '?') # removing integers
|
57
|
-
sql.gsub!(/\?(,\?)+/,'?') # replace multiple ? w/a single ?
|
58
|
-
sql
|
59
|
-
end
|
60
|
-
|
61
|
-
end # module ActiveRecordInstruments
|
62
|
-
end # module Instruments
|
49
|
+
end # module ActiveRecordInstruments
|
50
|
+
end # module Instruments
|
51
|
+
end
|
63
52
|
|
64
53
|
def add_instruments
|
65
54
|
if defined?(ActiveRecord) && defined?(ActiveRecord::Base)
|
@@ -82,4 +71,4 @@ if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i == 3 && ::Rails.respond_to?
|
|
82
71
|
end
|
83
72
|
else
|
84
73
|
add_instruments
|
85
|
-
end
|
74
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'scout_apm/environment'
|
2
|
+
|
3
|
+
# Removes actual values from SQL. Used to both obfuscate the SQL and group
|
4
|
+
# similar queries in the UI.
|
5
|
+
module ScoutApm
|
6
|
+
module Utils
|
7
|
+
class SqlSanitizer
|
8
|
+
attr_reader :sql
|
9
|
+
attr_accessor :database_engine
|
10
|
+
|
11
|
+
def initialize(sql)
|
12
|
+
@sql = sql.dup
|
13
|
+
@database_engine = ScoutApm::Environment.new.database_engine
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
return nil if sql.length > 1000 # safeguard - don't sanitize large SQL statements
|
18
|
+
case database_engine
|
19
|
+
when :postgres then to_s_postgres
|
20
|
+
when :mysql then to_s_mysql
|
21
|
+
when :sqlite then to_s_sqlite
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
MULTIPLE_SPACES = %r|\s+|.freeze
|
28
|
+
MULTIPLE_QUESTIONS = /\?(,\?)+/.freeze
|
29
|
+
TRAILING_SPACES = /\s+$/.freeze
|
30
|
+
|
31
|
+
PSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
|
32
|
+
PSQL_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
|
33
|
+
PSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
|
34
|
+
PSQL_PLACEHOLDER = /\$\d+/.freeze
|
35
|
+
|
36
|
+
def to_s_postgres
|
37
|
+
sql.gsub!(PSQL_PLACEHOLDER, '?')
|
38
|
+
sql.gsub!(PSQL_VAR_INTERPOLATION, '')
|
39
|
+
sql.gsub!(PSQL_REMOVE_STRINGS, '?')
|
40
|
+
sql.gsub!(PSQL_REMOVE_INTEGERS, '?')
|
41
|
+
sql.gsub!(MULTIPLE_SPACES, ' ')
|
42
|
+
sql.gsub!(TRAILING_SPACES, '')
|
43
|
+
sql
|
44
|
+
end
|
45
|
+
|
46
|
+
MYSQL_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
|
47
|
+
MYSQL_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
|
48
|
+
MYSQL_REMOVE_SINGLE_QUOTE_STRINGS = /'(?:[^']|'')*'/.freeze
|
49
|
+
MYSQL_REMOVE_DOUBLE_QUOTE_STRINGS = /"(?:[^"]|"")*"/.freeze
|
50
|
+
|
51
|
+
def to_s_mysql
|
52
|
+
sql.gsub!(MYSQL_VAR_INTERPOLATION, '')
|
53
|
+
sql.gsub!(MYSQL_REMOVE_SINGLE_QUOTE_STRINGS, '?')
|
54
|
+
sql.gsub!(MYSQL_REMOVE_DOUBLE_QUOTE_STRINGS, '?')
|
55
|
+
sql.gsub!(MYSQL_REMOVE_INTEGERS, '?')
|
56
|
+
sql.gsub!(MULTIPLE_QUESTIONS, '?')
|
57
|
+
sql.gsub!(TRAILING_SPACES, '')
|
58
|
+
sql
|
59
|
+
end
|
60
|
+
|
61
|
+
SQLITE_VAR_INTERPOLATION = %r|\[\[.*\]\]\s*$|.freeze
|
62
|
+
SQLITE_REMOVE_STRINGS = /'(?:[^']|'')*'/.freeze
|
63
|
+
SQLITE_REMOVE_INTEGERS = /(?<!LIMIT )\b\d+\b/.freeze
|
64
|
+
|
65
|
+
def to_s_sqlite
|
66
|
+
sql.gsub!(SQLITE_VAR_INTERPOLATION, '')
|
67
|
+
sql.gsub!(SQLITE_REMOVE_STRINGS, '?')
|
68
|
+
sql.gsub!(SQLITE_REMOVE_INTEGERS, '?')
|
69
|
+
sql.gsub!(MULTIPLE_SPACES, ' ')
|
70
|
+
sql.gsub!(TRAILING_SPACES, '')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/scout_apm/version.rb
CHANGED
data/scout_apm.gemspec
CHANGED
@@ -5,7 +5,7 @@ require "scout_apm/version"
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = "scout_apm"
|
7
7
|
s.version = ScoutApm::VERSION
|
8
|
-
s.authors = ["Derek Haynes",'Andre Lewis']
|
8
|
+
s.authors = ["Derek Haynes", 'Andre Lewis']
|
9
9
|
s.email = ["support@scoutapp.com"]
|
10
10
|
s.homepage = "https://github.com/scoutapp/scout_apm_ruby"
|
11
11
|
s.summary = "Ruby application performance monitoring"
|
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
19
|
s.require_paths = ["lib","data"]
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
s.add_development_dependency "minitest"
|
22
|
+
s.add_development_dependency "pry"
|
23
|
+
s.add_development_dependency "m"
|
24
24
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
common: &defaults
|
2
|
+
name: Scout APM
|
3
|
+
key: 000011110000
|
4
|
+
log_level: debug
|
5
|
+
monitor: true
|
6
|
+
|
7
|
+
production:
|
8
|
+
<<: *defaults
|
9
|
+
name: APM Test Conf (Production)
|
10
|
+
|
11
|
+
development:
|
12
|
+
<<: *defaults
|
13
|
+
name: APM Test Conf (Development)
|
14
|
+
host: http://localhost:3000
|
15
|
+
monitor: true
|
16
|
+
|
17
|
+
test:
|
18
|
+
<<: *defaults
|
19
|
+
name: APM Test Conf (Test)
|
20
|
+
monitor: false
|
21
|
+
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'minitest/unit'
|
4
|
+
require 'minitest/pride'
|
5
|
+
|
6
|
+
require 'pry'
|
7
|
+
|
8
|
+
Kernel.module_eval do
|
9
|
+
# Unset a constant without private access.
|
10
|
+
def self.const_unset(const)
|
11
|
+
self.instance_eval { remove_const(const) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# require 'scout_apm'
|
16
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'scout_apm/config'
|
4
|
+
|
5
|
+
class ConfigTest < Minitest::Test
|
6
|
+
def test_initalize_without_a_config
|
7
|
+
conf = ScoutApm::Config.new(nil)
|
8
|
+
|
9
|
+
# nil for random keys
|
10
|
+
assert_nil conf.value("log_file_path")
|
11
|
+
|
12
|
+
# but has values for defaulted keys
|
13
|
+
assert conf.value("host")
|
14
|
+
|
15
|
+
# and still reads from ENV
|
16
|
+
ENV['SCOUT_CONFIG_TEST_KEY'] = 'testval'
|
17
|
+
assert_equal 'testval', conf.value("config_test_key")
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_loading_a_file
|
21
|
+
ENV['RACK_ENV'] = "production"
|
22
|
+
conf_file = File.expand_path("../../data/config_test_1.yml", __FILE__)
|
23
|
+
conf = ScoutApm::Config.new(conf_file)
|
24
|
+
|
25
|
+
assert_equal "debug", conf.value('log_level')
|
26
|
+
assert_equal "APM Test Conf (Production)", conf.value('name')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'scout_apm/environment'
|
4
|
+
|
5
|
+
class EnvironmentTest < Minitest::Test
|
6
|
+
def teardown
|
7
|
+
clean_fake_rails
|
8
|
+
clean_fake_sinatra
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_framework_rails
|
12
|
+
fake_rails(2)
|
13
|
+
assert_equal :rails, ScoutApm::Environment.new.framework
|
14
|
+
|
15
|
+
clean_fake_rails
|
16
|
+
fake_rails(3)
|
17
|
+
assert_equal :rails3_or_4, ScoutApm::Environment.new.framework
|
18
|
+
|
19
|
+
clean_fake_rails
|
20
|
+
fake_rails(4)
|
21
|
+
assert_equal :rails3_or_4, ScoutApm::Environment.new.framework
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_framework_sinatra
|
25
|
+
fake_sinatra
|
26
|
+
assert_equal :sinatra, ScoutApm::Environment.new.framework
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_framework_ruby
|
30
|
+
assert_equal :ruby, ScoutApm::Environment.new.framework
|
31
|
+
end
|
32
|
+
|
33
|
+
############################################################
|
34
|
+
|
35
|
+
def fake_rails(version)
|
36
|
+
Kernel.const_set("Rails", Module.new)
|
37
|
+
Kernel.const_set("ActionController", Module.new)
|
38
|
+
r = Kernel.const_get("Rails")
|
39
|
+
r.const_set("VERSION", Module.new)
|
40
|
+
v = r.const_get("VERSION")
|
41
|
+
v.const_set("MAJOR", version)
|
42
|
+
|
43
|
+
assert_equal version, Rails::VERSION::MAJOR
|
44
|
+
end
|
45
|
+
|
46
|
+
def clean_fake_rails
|
47
|
+
Kernel.const_unset("Rails") if defined?(Kernel::Rails)
|
48
|
+
Kernel.const_unset("ActionController") if defined?(Kernel::ActionController)
|
49
|
+
end
|
50
|
+
|
51
|
+
def fake_sinatra
|
52
|
+
Kernel.const_set("Sinatra", Module.new)
|
53
|
+
s = Kernel.const_get("Sinatra")
|
54
|
+
s.const_set("Base", Module.new)
|
55
|
+
end
|
56
|
+
|
57
|
+
def clean_fake_sinatra
|
58
|
+
Kernel.const_unset("Sinatra") if defined?(Kernel::Sinatra)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
require 'scout_apm/utils/sql_sanitizer'
|
4
|
+
|
5
|
+
module ScoutApm
|
6
|
+
module Utils
|
7
|
+
class SqlSanitizerTest < Minitest::Test
|
8
|
+
# Too long, and we just bail out to prevent long running instrumentation
|
9
|
+
def test_long_sql
|
10
|
+
sql = " " * 1001
|
11
|
+
assert_nil SqlSanitizer.new(sql).to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_postgres_simple_select_of_first
|
15
|
+
sql = %q|SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1|
|
16
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
|
17
|
+
assert_equal %q|SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1|, ss.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_postgres_where
|
21
|
+
sql = %q|SELECT "users".* FROM "users" WHERE "users"."name" = $1 [["name", "chris"]]|
|
22
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
|
23
|
+
assert_equal %q|SELECT "users".* FROM "users" WHERE "users"."name" = ?|, ss.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_postgres_strips_literals
|
27
|
+
# Strip strings
|
28
|
+
sql = %q|SELECT "users".* FROM "users" INNER JOIN "blogs" ON "blogs"."user_id" = "users"."id" WHERE (blogs.title = 'hello world')|
|
29
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
|
30
|
+
assert_equal %q|SELECT "users".* FROM "users" INNER JOIN "blogs" ON "blogs"."user_id" = "users"."id" WHERE (blogs.title = ?)|, ss.to_s
|
31
|
+
|
32
|
+
# Strip integers
|
33
|
+
sql = %q|SELECT "blogs".* FROM "blogs" WHERE (view_count > 10)|
|
34
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :postgres }
|
35
|
+
assert_equal %q|SELECT "blogs".* FROM "blogs" WHERE (view_count > ?)|, ss.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_mysql_where
|
39
|
+
sql = %q|SELECT `users`.* FROM `users` WHERE `users`.`name` = ? [["name", "chris"]]|
|
40
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :mysql }
|
41
|
+
assert_equal %q|SELECT `users`.* FROM `users` WHERE `users`.`name` = ?|, ss.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_mysql_limit
|
45
|
+
sql = %q|SELECT `blogs`.* FROM `blogs` ORDER BY `blogs`.`id` ASC LIMIT 1|
|
46
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :mysql }
|
47
|
+
assert_equal %q|SELECT `blogs`.* FROM `blogs` ORDER BY `blogs`.`id` ASC LIMIT 1|, ss.to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_mysql_literals
|
51
|
+
sql = %q|SELECT `blogs`.* FROM `blogs` WHERE (title = 'abc')|
|
52
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :mysql }
|
53
|
+
assert_equal %q|SELECT `blogs`.* FROM `blogs` WHERE (title = ?)|, ss.to_s
|
54
|
+
|
55
|
+
sql = %q|SELECT `blogs`.* FROM `blogs` WHERE (title = "abc")|
|
56
|
+
ss = SqlSanitizer.new(sql).tap{ |it| it.database_engine = :mysql }
|
57
|
+
assert_equal %q|SELECT `blogs`.* FROM `blogs` WHERE (title = ?)|, ss.to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
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: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Derek Haynes
|
@@ -9,8 +9,50 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-08-
|
13
|
-
dependencies:
|
12
|
+
date: 2015-08-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minitest
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: pry
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: m
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
14
56
|
description: Monitors Ruby apps and reports detailed metrics on performance to Scout.
|
15
57
|
email:
|
16
58
|
- support@scoutapp.com
|
@@ -49,8 +91,16 @@ files:
|
|
49
91
|
- lib/scout_apm/stack_item.rb
|
50
92
|
- lib/scout_apm/store.rb
|
51
93
|
- lib/scout_apm/tracer.rb
|
94
|
+
- lib/scout_apm/utils/null_logger.rb
|
95
|
+
- lib/scout_apm/utils/sql_sanitizer.rb
|
52
96
|
- lib/scout_apm/version.rb
|
53
97
|
- scout_apm.gemspec
|
98
|
+
- test/data/config_test_1.yml
|
99
|
+
- test/test_helper.rb
|
100
|
+
- test/unit/config_test.rb
|
101
|
+
- test/unit/environment_test.rb
|
102
|
+
- test/unit/instruments/active_record_instruments_test.rb
|
103
|
+
- test/unit/sql_sanitizer_test.rb
|
54
104
|
homepage: https://github.com/scoutapp/scout_apm_ruby
|
55
105
|
licenses: []
|
56
106
|
metadata: {}
|
@@ -71,9 +121,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
121
|
version: '0'
|
72
122
|
requirements: []
|
73
123
|
rubyforge_project: scout_apm
|
74
|
-
rubygems_version: 2.
|
124
|
+
rubygems_version: 2.2.2
|
75
125
|
signing_key:
|
76
126
|
specification_version: 4
|
77
127
|
summary: Ruby application performance monitoring
|
78
|
-
test_files:
|
79
|
-
|
128
|
+
test_files:
|
129
|
+
- test/data/config_test_1.yml
|
130
|
+
- test/test_helper.rb
|
131
|
+
- test/unit/config_test.rb
|
132
|
+
- test/unit/environment_test.rb
|
133
|
+
- test/unit/instruments/active_record_instruments_test.rb
|
134
|
+
- test/unit/sql_sanitizer_test.rb
|