scout_apm 0.1.3.1 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|