scout_apm 0.1.17 → 0.1.18.stackprof4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.markdown +0 -4
- data/lib/scout_apm.rb +21 -0
- data/lib/scout_apm/agent.rb +9 -0
- data/lib/scout_apm/config.rb +17 -4
- data/lib/scout_apm/deploy_integrations/capistrano_2.cap +12 -0
- data/lib/scout_apm/deploy_integrations/capistrano_2.rb +83 -0
- data/lib/scout_apm/deploy_integrations/capistrano_3.cap +12 -0
- data/lib/scout_apm/deploy_integrations/capistrano_3.rb +82 -0
- data/lib/scout_apm/environment.rb +20 -3
- data/lib/scout_apm/instruments/action_controller_rails_3.rb +22 -10
- data/lib/scout_apm/reporter.rb +21 -2
- data/lib/scout_apm/serializers/deploy_serializer.rb +16 -0
- data/lib/scout_apm/slow_transaction.rb +42 -29
- data/lib/scout_apm/stackprof_tree_collapser.rb +218 -0
- data/lib/scout_apm/store.rb +15 -7
- data/lib/scout_apm/tracer.rb +15 -3
- data/lib/scout_apm/utils/fake_stack_prof.rb +36 -0
- data/lib/scout_apm/version.rb +1 -1
- data/scout_apm.gemspec +2 -0
- metadata +31 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7040acd30e2e30aceb89d48e43c699e007d87ea5
|
4
|
+
data.tar.gz: 76222f981eb632a50948c4f765ad387c671bbf80
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 811ca92ad91009f8e3577caa8641a7b8c6e189bf2460b871f24c1d98d9e5a8b75376194391729aa42d47e3a47e9bf138dfcb34f0cd316f346281b870fd50e902
|
7
|
+
data.tar.gz: 5b0e5825e3d97e4ba7ce950792890bc5318be6e5fb23d60e6ad4c8bbeac6f1599a14d2f8ecf721c44cc56f046b711b090e322f8f87133dadf912220eeef7f613
|
data/CHANGELOG.markdown
CHANGED
data/lib/scout_apm.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
module ScoutApm
|
2
2
|
end
|
3
3
|
|
4
|
+
#####################################
|
5
|
+
# Ruby StdLibrary Requires
|
6
|
+
#####################################
|
4
7
|
require 'cgi'
|
5
8
|
require 'logger'
|
6
9
|
require 'net/http'
|
@@ -10,6 +13,18 @@ require 'socket'
|
|
10
13
|
require 'yaml'
|
11
14
|
require 'thread'
|
12
15
|
|
16
|
+
#####################################
|
17
|
+
# Gem Requires
|
18
|
+
#####################################
|
19
|
+
begin
|
20
|
+
require 'stackprof'
|
21
|
+
rescue LoadError
|
22
|
+
require 'scout_apm/utils/fake_stack_prof'
|
23
|
+
end
|
24
|
+
|
25
|
+
#####################################
|
26
|
+
# Internal Requires
|
27
|
+
#####################################
|
13
28
|
require 'scout_apm/version'
|
14
29
|
|
15
30
|
require 'scout_apm/server_integrations/passenger'
|
@@ -25,6 +40,9 @@ require 'scout_apm/framework_integrations/rails_3_or_4'
|
|
25
40
|
require 'scout_apm/framework_integrations/sinatra'
|
26
41
|
require 'scout_apm/framework_integrations/ruby'
|
27
42
|
|
43
|
+
require 'scout_apm/deploy_integrations/capistrano_3'
|
44
|
+
#require 'scout_apm/deploy_integrations/capistrano_2'
|
45
|
+
|
28
46
|
require 'scout_apm/instruments/net_http'
|
29
47
|
require 'scout_apm/instruments/moped'
|
30
48
|
require 'scout_apm/instruments/mongoid'
|
@@ -41,6 +59,7 @@ require 'scout_apm/utils/sql_sanitizer'
|
|
41
59
|
require 'scout_apm/utils/null_logger'
|
42
60
|
require 'scout_apm/utils/installed_gems'
|
43
61
|
require 'scout_apm/utils/time'
|
62
|
+
|
44
63
|
require 'scout_apm/config'
|
45
64
|
require 'scout_apm/environment'
|
46
65
|
require 'scout_apm/agent'
|
@@ -56,12 +75,14 @@ require 'scout_apm/stack_item'
|
|
56
75
|
require 'scout_apm/store'
|
57
76
|
require 'scout_apm/tracer'
|
58
77
|
require 'scout_apm/context'
|
78
|
+
require 'scout_apm/stackprof_tree_collapser'
|
59
79
|
require 'scout_apm/slow_transaction'
|
60
80
|
require 'scout_apm/capacity'
|
61
81
|
|
62
82
|
require 'scout_apm/serializers/payload_serializer'
|
63
83
|
require 'scout_apm/serializers/directive_serializer'
|
64
84
|
require 'scout_apm/serializers/app_server_load_serializer'
|
85
|
+
require 'scout_apm/serializers/deploy_serializer'
|
65
86
|
|
66
87
|
if defined?(Rails) and Rails.respond_to?(:version) and Rails.version >= '3'
|
67
88
|
module ScoutApm
|
data/lib/scout_apm/agent.rb
CHANGED
@@ -78,6 +78,11 @@ module ScoutApm
|
|
78
78
|
init_logger
|
79
79
|
logger.info "Attempting to start Scout Agent [#{ScoutApm::VERSION}] on [#{environment.hostname}]"
|
80
80
|
|
81
|
+
if environment.deploy_integration
|
82
|
+
logger.info "Starting monitoring for [#{environment.deploy_integration.name}]]."
|
83
|
+
return environment.deploy_integration.install
|
84
|
+
end
|
85
|
+
|
81
86
|
return false unless preconditions_met?
|
82
87
|
|
83
88
|
@started = true
|
@@ -197,5 +202,9 @@ module ScoutApm
|
|
197
202
|
@installed_instruments << instance
|
198
203
|
instance.install
|
199
204
|
end
|
205
|
+
|
206
|
+
def deploy_integration
|
207
|
+
environment.deploy_integration
|
208
|
+
end
|
200
209
|
end
|
201
210
|
end
|
data/lib/scout_apm/config.rb
CHANGED
@@ -21,6 +21,7 @@ module ScoutApm
|
|
21
21
|
DEFAULTS = {
|
22
22
|
'host' => 'https://apm.scoutapp.com',
|
23
23
|
'log_level' => 'info',
|
24
|
+
'stackprof_interval' => 20000 # microseconds, 1000 = 1 millisecond, so 20k == 20 milliseconds
|
24
25
|
}.freeze
|
25
26
|
|
26
27
|
def initialize(config_path = nil)
|
@@ -30,8 +31,16 @@ module ScoutApm
|
|
30
31
|
# Fetch a config value.
|
31
32
|
# It first attempts to fetch an ENV var prefixed with 'SCOUT_',
|
32
33
|
# then from the settings file.
|
33
|
-
|
34
|
-
|
34
|
+
#
|
35
|
+
# If you set env_only, then it will not attempt to read the config file at
|
36
|
+
# all, and only read off the ENV var this is useful to break a loop during
|
37
|
+
# boot, where we needed an option to set the application root.
|
38
|
+
def value(key, env_only=false)
|
39
|
+
value = ENV['SCOUT_'+key.upcase]
|
40
|
+
if !value && !env_only
|
41
|
+
value = setting(key)
|
42
|
+
end
|
43
|
+
|
35
44
|
value.to_s.strip.length.zero? ? nil : value
|
36
45
|
end
|
37
46
|
|
@@ -45,8 +54,12 @@ module ScoutApm
|
|
45
54
|
File.expand_path(config_path)
|
46
55
|
end
|
47
56
|
|
48
|
-
def
|
49
|
-
|
57
|
+
def setting(key)
|
58
|
+
settings[key] || settings(true)[key]
|
59
|
+
end
|
60
|
+
|
61
|
+
def settings(try_reload=false)
|
62
|
+
(@settings.nil? || try_reload) ? @settings = load_file : @settings
|
50
63
|
end
|
51
64
|
|
52
65
|
def config_environment
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :scout_apm do
|
2
|
+
namespace :deploy do
|
3
|
+
task :starting do
|
4
|
+
# Warn if missing scout apm deploy creds?
|
5
|
+
end
|
6
|
+
task :finished do
|
7
|
+
ScoutApm::Agent.instance.deploy_integration.report
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
after 'deploy:finished', 'scout_apm:deploy:finished'
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'scout_apm'
|
2
|
+
|
3
|
+
module ScoutApm
|
4
|
+
module DeployIntegrations
|
5
|
+
class Capistrano2
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
def initialize(logger)
|
9
|
+
@logger = logger
|
10
|
+
@cap = defined?(Capistrano::Configuration) ? ObjectSpace.each_object(Capistrano::Configuration).map.first : nil rescue nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
:capistrano_2
|
15
|
+
end
|
16
|
+
|
17
|
+
def version
|
18
|
+
present? ? Capistrano::VERSION : nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def present?
|
22
|
+
if !@cap.nil? && @cap.is_a?(Capistrano::Configuration)
|
23
|
+
require 'capistrano/version'
|
24
|
+
defined?(Capistrano::VERSION) && Gem::Dependency.new('', '~> 2.0').match?('', Capistrano::VERSION.to_s)
|
25
|
+
else
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
return true
|
29
|
+
rescue
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
def install
|
34
|
+
logger.debug "Initializing Capistrano2 Deploy Integration."
|
35
|
+
@cap.load File.expand_path("../capistrano_2.cap", __FILE__)
|
36
|
+
end
|
37
|
+
|
38
|
+
def root
|
39
|
+
'.'
|
40
|
+
end
|
41
|
+
|
42
|
+
def env
|
43
|
+
@cap.fetch(:stage)
|
44
|
+
end
|
45
|
+
|
46
|
+
def found?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def report
|
51
|
+
if reporter.can_report?
|
52
|
+
data = deploy_data
|
53
|
+
logger.debug "Sending deploy hook data: #{data}"
|
54
|
+
payload = ScoutApm::Serializers::DeploySerializer.serialize(data)
|
55
|
+
reporter.report(payload, ScoutApm::Serializers::DeploySerializer::HTTP_HEADERS)
|
56
|
+
else
|
57
|
+
logger.warn "Unable to post deploy hook data"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def reporter
|
62
|
+
@reporter ||= ScoutApm::Reporter.new(:deploy_hook, ScoutApm::Agent.instance.config, @logger)
|
63
|
+
end
|
64
|
+
|
65
|
+
def deploy_data
|
66
|
+
{:revision => current_revision, :branch => branch, :deployed_by => deployed_by}
|
67
|
+
end
|
68
|
+
|
69
|
+
def branch
|
70
|
+
@cap.fetch(:branch)
|
71
|
+
end
|
72
|
+
|
73
|
+
def current_revision
|
74
|
+
@cap.fetch(:current_revision) || `git rev-list --max-count=1 --abbrev-commit --abbrev=12 #{branch}`.chomp
|
75
|
+
end
|
76
|
+
|
77
|
+
def deployed_by
|
78
|
+
ScoutApm::Agent.instance.config.value('deployed_by')
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :scout_apm do
|
2
|
+
namespace :deploy do
|
3
|
+
task :starting do
|
4
|
+
# Warn if missing scout apm deploy creds?
|
5
|
+
end
|
6
|
+
task :finished do
|
7
|
+
ScoutApm::Agent.instance.deploy_integration.report
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
after 'deploy:finished', 'scout_apm:deploy:finished'
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'scout_apm'
|
2
|
+
|
3
|
+
module ScoutApm
|
4
|
+
module DeployIntegrations
|
5
|
+
class Capistrano3
|
6
|
+
attr_reader :logger
|
7
|
+
|
8
|
+
def initialize(logger)
|
9
|
+
@logger = logger
|
10
|
+
@cap = Rake.application rescue nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
:capistrano_3
|
15
|
+
end
|
16
|
+
|
17
|
+
def version
|
18
|
+
present? ? Capistrano::VERSION : nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def present?
|
22
|
+
if !@cap.nil? && @cap.is_a?(Capistrano::Application)
|
23
|
+
require 'capistrano/version'
|
24
|
+
defined?(Capistrano::VERSION) && Gem::Dependency.new('', '~> 3.0').match?('', Capistrano::VERSION.to_s)
|
25
|
+
else
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
rescue
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
|
32
|
+
def install
|
33
|
+
logger.debug "Initializing Capistrano3 Deploy Integration."
|
34
|
+
load File.expand_path("../capistrano_3.cap", __FILE__)
|
35
|
+
end
|
36
|
+
|
37
|
+
def root
|
38
|
+
'.'
|
39
|
+
end
|
40
|
+
|
41
|
+
def env
|
42
|
+
@cap.fetch(:stage).to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
def found?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def report
|
50
|
+
if reporter.can_report?
|
51
|
+
data = deploy_data
|
52
|
+
logger.debug "Sending deploy hook data: #{data}"
|
53
|
+
payload = ScoutApm::Serializers::DeploySerializer.serialize(data)
|
54
|
+
reporter.report(payload, ScoutApm::Serializers::DeploySerializer::HTTP_HEADERS)
|
55
|
+
else
|
56
|
+
logger.warn "Unable to post deploy hook data"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def reporter
|
61
|
+
@reporter ||= ScoutApm::Reporter.new(:deploy_hook, ScoutApm::Agent.instance.config, @logger)
|
62
|
+
end
|
63
|
+
|
64
|
+
def deploy_data
|
65
|
+
{:revision => current_revision, :branch => branch, :deployed_by => deployed_by}
|
66
|
+
end
|
67
|
+
|
68
|
+
def branch
|
69
|
+
@cap.fetch(:branch)
|
70
|
+
end
|
71
|
+
|
72
|
+
def current_revision
|
73
|
+
@cap.fetch(:current_revision) || `git rev-list --max-count=1 --abbrev-commit --abbrev=12 #{branch}`.chomp
|
74
|
+
end
|
75
|
+
|
76
|
+
def deployed_by
|
77
|
+
ScoutApm::Agent.instance.config.value('deployed_by')
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -24,8 +24,13 @@ module ScoutApm
|
|
24
24
|
ScoutApm::FrameworkIntegrations::Ruby.new, # Fallback if none match
|
25
25
|
]
|
26
26
|
|
27
|
+
DEPLOY_INTEGRATIONS = [
|
28
|
+
ScoutApm::DeployIntegrations::Capistrano3.new(Logger.new(STDOUT)),
|
29
|
+
# ScoutApm::DeployIntegrations::Capistrano2.new(Logger.new(STDOUT)),
|
30
|
+
]
|
31
|
+
|
27
32
|
def env
|
28
|
-
@env ||= framework_integration.env
|
33
|
+
@env ||= deploy_integration? ? deploy_integration.env : framework_integration.env
|
29
34
|
end
|
30
35
|
|
31
36
|
def framework
|
@@ -58,10 +63,14 @@ module ScoutApm
|
|
58
63
|
end
|
59
64
|
|
60
65
|
def root
|
61
|
-
|
66
|
+
return deploy_integration.root if deploy_integration
|
67
|
+
framework_root
|
68
|
+
end
|
69
|
+
|
70
|
+
def framework_root
|
71
|
+
if override_root = Agent.instance.config.value("application_root", true)
|
62
72
|
return override_root
|
63
73
|
end
|
64
|
-
|
65
74
|
if framework == :rails
|
66
75
|
RAILS_ROOT.to_s
|
67
76
|
elsif framework == :rails3_or_4
|
@@ -102,6 +111,14 @@ module ScoutApm
|
|
102
111
|
app_server_integration.forking?
|
103
112
|
end
|
104
113
|
|
114
|
+
def deploy_integration
|
115
|
+
@deploy_integration ||= DEPLOY_INTEGRATIONS.detect{ |integration| integration.present? }
|
116
|
+
end
|
117
|
+
|
118
|
+
def deploy_integration?
|
119
|
+
!@deploy_integration.nil?
|
120
|
+
end
|
121
|
+
|
105
122
|
### ruby checks
|
106
123
|
|
107
124
|
def rubinius?
|
@@ -27,12 +27,25 @@ module ScoutApm
|
|
27
27
|
|
28
28
|
if defined?(::ActionView) && defined?(::ActionView::PartialRenderer)
|
29
29
|
ScoutApm::Agent.instance.logger.debug "Instrumenting ActionView::PartialRenderer"
|
30
|
-
ActionView::PartialRenderer.class_eval do
|
30
|
+
::ActionView::PartialRenderer.class_eval do
|
31
31
|
include ScoutApm::Tracer
|
32
|
-
instrument_method :render_partial,
|
32
|
+
instrument_method :render_partial,
|
33
|
+
:metric_name => 'View/#{@template.virtual_path rescue "Unknown Partial"}/Rendering',
|
34
|
+
:scope => true
|
35
|
+
|
36
|
+
instrument_method :collection_with_template,
|
37
|
+
:metric_name => 'View/#{@template.virtual_path rescue "Unknown Collection"}/Rendering',
|
38
|
+
:scope => true
|
33
39
|
end
|
34
|
-
end
|
35
40
|
|
41
|
+
ScoutApm::Agent.instance.logger.debug "Instrumenting ActionView::TemplateRenderer"
|
42
|
+
::ActionView::TemplateRenderer.class_eval do
|
43
|
+
include ScoutApm::Tracer
|
44
|
+
instrument_method :render_template,
|
45
|
+
:metric_name => 'View/#{args[0].virtual_path rescue "Unknown"}/Rendering',
|
46
|
+
:scope => true
|
47
|
+
end
|
48
|
+
end
|
36
49
|
end
|
37
50
|
end
|
38
51
|
|
@@ -42,13 +55,18 @@ module ScoutApm
|
|
42
55
|
scout_controller_action = "Controller/#{controller_path}/#{action_name}"
|
43
56
|
|
44
57
|
self.class.scout_apm_trace(scout_controller_action, :uri => request.fullpath, :ip => request.remote_ip) do
|
58
|
+
Thread::current[:scout_apm_prof] = nil
|
59
|
+
StackProf.start(mode: :wall, interval: ScoutApm::Agent.instance.config.value("stackprof_interval"))
|
60
|
+
|
45
61
|
begin
|
46
62
|
super
|
47
63
|
rescue Exception
|
48
|
-
ScoutApm::Agent.instance.store.track!("Errors/Request",1, :scope => nil)
|
64
|
+
ScoutApm::Agent.instance.store.track!("Errors/Request", 1, :scope => nil)
|
49
65
|
raise
|
50
66
|
ensure
|
51
67
|
Thread::current[:scout_apm_scope_name] = nil
|
68
|
+
StackProf.stop
|
69
|
+
Thread::current[:scout_apm_prof] = StackProf.results
|
52
70
|
end
|
53
71
|
end
|
54
72
|
end
|
@@ -56,9 +74,3 @@ module ScoutApm
|
|
56
74
|
end
|
57
75
|
end
|
58
76
|
|
59
|
-
|
60
|
-
# Rails 3/4
|
61
|
-
module ScoutApm
|
62
|
-
module Instruments
|
63
|
-
end
|
64
|
-
end
|
data/lib/scout_apm/reporter.rb
CHANGED
@@ -16,8 +16,8 @@ module ScoutApm
|
|
16
16
|
end
|
17
17
|
|
18
18
|
# TODO: Parse & return a real response object, not the HTTP Response object
|
19
|
-
def report(payload)
|
20
|
-
post(uri, payload)
|
19
|
+
def report(payload, headers = {})
|
20
|
+
post(uri, payload, headers)
|
21
21
|
end
|
22
22
|
|
23
23
|
def uri
|
@@ -26,9 +26,26 @@ module ScoutApm
|
|
26
26
|
URI.parse("#{config.value('host')}/apps/checkin.scout?key=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
|
27
27
|
when :app_server_load
|
28
28
|
URI.parse("#{config.value('host')}/apps/app_server_load.scout?key=#{config.value('key')}&name=#{CGI.escape(Environment.instance.application_name)}")
|
29
|
+
when :deploy_hook
|
30
|
+
URI.parse("#{config.value('host')}/apps/deploy.scout?key=#{config.value('key')}&name=#{CGI.escape(config.value('name'))}")
|
29
31
|
end.tap{|u| logger.debug("Posting to #{u.to_s}")}
|
30
32
|
end
|
31
33
|
|
34
|
+
def can_report?
|
35
|
+
case type
|
36
|
+
when :deploy_hook
|
37
|
+
%w(host key name).each do |k|
|
38
|
+
if config.value(k).nil?
|
39
|
+
logger.warn "/#{type} FAILED: missing required config value for #{k}"
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
return true
|
44
|
+
else
|
45
|
+
return true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
32
49
|
private
|
33
50
|
|
34
51
|
def post(uri, body, headers = Hash.new)
|
@@ -52,6 +69,8 @@ module ScoutApm
|
|
52
69
|
logger.debug "/#{type} OK"
|
53
70
|
when Net::HTTPBadRequest
|
54
71
|
logger.warn "/#{type} FAILED: The Account Key [#{config.value('key')}] is invalid."
|
72
|
+
when Net::HTTPUnprocessableEntity
|
73
|
+
logger.warn "/#{type} FAILED: #{response.body}"
|
55
74
|
else
|
56
75
|
logger.debug "/#{type} FAILED: #{response.inspect}"
|
57
76
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# Serialize & deserialize deploy data up to the APM server
|
2
|
+
module ScoutApm
|
3
|
+
module Serializers
|
4
|
+
class DeploySerializer
|
5
|
+
HTTP_HEADERS = {'Content-Type' => 'application/x-www-form-urlencoded'}
|
6
|
+
|
7
|
+
def self.serialize(data)
|
8
|
+
URI.encode_www_form(data)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.deserialize(data)
|
12
|
+
Marshal.load(data)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -1,36 +1,49 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module ScoutApm
|
2
|
+
class SlowTransaction
|
3
|
+
BACKTRACE_THRESHOLD = 0.5 # the minimum threshold to record the backtrace for a metric.
|
4
|
+
BACKTRACE_LIMIT = 5 # Max length of callers to display
|
5
|
+
MAX_SIZE = 100 # Limits the size of the metric hash to prevent a metric explosion.
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
attr_reader :metric_name
|
8
|
+
attr_reader :total_call_time
|
9
|
+
attr_reader :metrics
|
10
|
+
attr_reader :meta
|
11
|
+
attr_reader :uri
|
12
|
+
attr_reader :context
|
13
|
+
attr_reader :time
|
14
|
+
attr_reader :prof
|
15
|
+
attr_reader :raw_prof
|
16
|
+
|
17
|
+
# Given a call stack, generates a filtered backtrace that:
|
18
|
+
# * Limits to the app/models, app/controllers, or app/views directories
|
19
|
+
# * Limits to 5 total callers
|
20
|
+
# * Makes the app folder the top-level folder used in trace info
|
21
|
+
def self.backtrace_parser(backtrace)
|
22
|
+
stack = []
|
23
|
+
backtrace.each do |c|
|
24
|
+
if m=c.match(/(\/app\/(controllers|models|views)\/.+)/)
|
25
|
+
stack << m[1]
|
26
|
+
break if stack.size == BACKTRACE_LIMIT
|
27
|
+
end
|
17
28
|
end
|
29
|
+
stack
|
18
30
|
end
|
19
|
-
stack
|
20
|
-
end
|
21
31
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
32
|
+
def initialize(uri, metric_name, total_call_time, metrics, context, time, raw_stackprof)
|
33
|
+
@uri = uri
|
34
|
+
@metric_name = metric_name
|
35
|
+
@total_call_time = total_call_time
|
36
|
+
@metrics = metrics
|
37
|
+
@context = context
|
38
|
+
@time = time
|
39
|
+
@prof = ScoutApm::StackprofTreeCollapser.new(raw_stackprof).call
|
40
|
+
@raw_prof = raw_stackprof # Send whole data up to server
|
41
|
+
end
|
30
42
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
43
|
+
# Used to remove metrics when the payload will be too large.
|
44
|
+
def clear_metrics!
|
45
|
+
@metrics = nil
|
46
|
+
self
|
47
|
+
end
|
35
48
|
end
|
36
49
|
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# require 'json'; p = JSON::parse(File.read("/Users/cschneid/example_stackprof.out")); p=p.with_indifferent_access; ScoutApm::StackprofTreeCollapser.new(p).call
|
2
|
+
# require 'json'; p = JSON::parse(File.read("/Users/cschneid/profile_appscontroller.json")); p=p.with_indifferent_access; ScoutApm::StackprofTreeCollapser.new(p).call
|
3
|
+
# require 'json'; p = JSON::parse(File.read("/Users/cschneid/profile_elasticsearch.json")); p=p.with_indifferent_access; ScoutApm::StackprofTreeCollapser.new(p).call
|
4
|
+
|
5
|
+
# in_app_nodes.map{|n| [n.samples_for_self_and_descendants, n.name, n.file, n.line]}.sort_by {|x| x[0] }
|
6
|
+
# in_app_nodes.map{|n| [n.total_samples, n.name] }.sort_by {|x| x[0] }
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
module ScoutApm
|
11
|
+
class StackprofTreeCollapser
|
12
|
+
attr_reader :raw_stackprof
|
13
|
+
attr_reader :nodes # the current set of nodes under consideration
|
14
|
+
|
15
|
+
def initialize(raw_stackprof)
|
16
|
+
@raw_stackprof = raw_stackprof
|
17
|
+
ScoutApm::Agent.instance.logger.info("StackProf - Samples: #{raw_stackprof[:samples]}, GC: #{raw_stackprof[:gc_samples]}, missed: #{raw_stackprof[:missed_samples]}, Interval: #{raw_stackprof[:interval]}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
build_tree
|
22
|
+
connect_children
|
23
|
+
total_samples_of_app_nodes
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def build_tree
|
29
|
+
@nodes = raw_stackprof[:frames].map do |(frame_id, frame_data)|
|
30
|
+
TreeNode.new(frame_id, # frame_id
|
31
|
+
frame_data[:name], # name
|
32
|
+
frame_data[:file], # file
|
33
|
+
frame_data[:line], # line
|
34
|
+
frame_data[:samples], # samples
|
35
|
+
frame_data[:total_samples], # total_samples
|
36
|
+
(frame_data[:edges] || {}), # children_edges [ { id => weight } ]
|
37
|
+
[], # children [ treenode, ... ]
|
38
|
+
[] # parents [ [treenode, int (weight) ], [...] ]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def connect_children
|
44
|
+
nodes.each do |node|
|
45
|
+
children = nodes.find_all { |n| node.children_edges.keys.include? n.frame_id }
|
46
|
+
|
47
|
+
node.children_edges.each do |(frame_id, weight)|
|
48
|
+
child = children.detect{ |c| c.frame_id == frame_id }
|
49
|
+
child.parents << [node, weight]
|
50
|
+
end
|
51
|
+
|
52
|
+
node.children = children
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def in_app_nodes
|
57
|
+
nodes.select {|n| n.app? }
|
58
|
+
end
|
59
|
+
|
60
|
+
def total_samples_of_app_nodes
|
61
|
+
in_app_nodes.reject{|n| n.calls_only_app_nodes? && !n.has_samples? }.
|
62
|
+
map{|n| { samples: n.total_samples,
|
63
|
+
name: n.name,
|
64
|
+
file: n.file,
|
65
|
+
line: n.line
|
66
|
+
}
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
# @results will be [{name, samples, file, line}]
|
71
|
+
# def calculate_results
|
72
|
+
# @results = in_app_nodes.map do |node|
|
73
|
+
# desc = node.all_descendants
|
74
|
+
# total_samples = desc.map(&:samples).sum
|
75
|
+
# { desc_count: desc.length, name: node.name, file: node.file, line: node.line, samples: total_samples }
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
|
79
|
+
# def collapse_tree
|
80
|
+
# while true
|
81
|
+
# number_changed = collapse_tree_one_level
|
82
|
+
# break if number_changed == 0
|
83
|
+
# end
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# # For each leaf node, sees if it is internal to the monitored app. If not,
|
87
|
+
# # collapse that node to its parents, weighted by the edge counts
|
88
|
+
# # If that node was internal to the monitored app, leave it.
|
89
|
+
# # Returns 0 if nothing changed, a positive integer if things did change,
|
90
|
+
# # indicating how many leaves were collapsed
|
91
|
+
# def collapse_tree_one_level
|
92
|
+
# number_changed = 0
|
93
|
+
#
|
94
|
+
# puts "===========ITERATION==========="
|
95
|
+
# leaves.each do |leaf_node|
|
96
|
+
# next if leaf_node.app?
|
97
|
+
# puts "Collapsing - #{leaf_node.name}"
|
98
|
+
# # app parent: #{leaf_node.self_or_parents_in_app?.map {|x| x.name}}"
|
99
|
+
# number_changed += 1
|
100
|
+
# leaf_node.collapse_to_parent!
|
101
|
+
# @nodes = @nodes.reject { |n| n == leaf_node }
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# number_changed
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# # Returns the final result, an array of hashes
|
108
|
+
# def generate_output
|
109
|
+
# leaves.map{|x| { name: x.name, samples: x.samples, file: x.file, line: x.line } }
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# # A leaf node has no children.
|
113
|
+
# def leaves
|
114
|
+
# nodes.find_all { |n| n.children.empty? }
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
|
118
|
+
###########################################
|
119
|
+
# TreeNode class represents a single node.
|
120
|
+
###########################################
|
121
|
+
TreeNode = Struct.new(:frame_id, :name, :file, :line, :samples, :total_samples,
|
122
|
+
:children_edges, :children, :parents) do
|
123
|
+
def app?
|
124
|
+
@is_app ||= file =~ /^#{ScoutApm::Environment.instance.root}/
|
125
|
+
end
|
126
|
+
|
127
|
+
# My samples, and the weighted samples of all of my children
|
128
|
+
#def samples_for_self_and_descendants(seen=Set.new)
|
129
|
+
# viable_children = children.reject(&:app?)
|
130
|
+
# @samples_for_self_and_descendants ||= samples + viable_children.map{ |c_node|
|
131
|
+
# if seen.include? self
|
132
|
+
# puts "I've already seen #{self.name}, bailing"
|
133
|
+
# return samples # we've already been included, we're looping
|
134
|
+
# else
|
135
|
+
# seen << self
|
136
|
+
# c_node.samples_for_parent(self, seen.dup).tap { |val| puts "Child gave me #{val}" }
|
137
|
+
# end
|
138
|
+
# }.sum
|
139
|
+
#end
|
140
|
+
|
141
|
+
# For this parent of mine, how many of my samples do they get.
|
142
|
+
# is combo of "how many samples do I have, and what's the relative weight of this parent"
|
143
|
+
#def samples_for_parent(p_node, seen=Set.new)
|
144
|
+
# samples_for_self_and_descendants(seen) * relative_weight_of_parent(p_node)
|
145
|
+
#end
|
146
|
+
|
147
|
+
#def relative_weight_of_parent(p_node)
|
148
|
+
# total = parents.map{|(_, weight)| weight}.sum
|
149
|
+
# p_node_weight = parents.detect(0) {|(this_parent, _)| this_parent == p_node }[1]
|
150
|
+
# p_node_weight.to_f / total.to_f
|
151
|
+
#end
|
152
|
+
|
153
|
+
# Allocate this node's samples to its parents, in relation to the rate at
|
154
|
+
# which each parent called this method. Then clear the child from each of the parents
|
155
|
+
#def collapse_to_parent!
|
156
|
+
# total_weight = parents.map{ |(_, weight)| weight }.inject(0){ |sum, weight| sum + weight }
|
157
|
+
# parents.each do |(p_node, weight)|
|
158
|
+
# relative_weight = weight.to_f / total_weight.to_f
|
159
|
+
# p_node.samples += (samples * relative_weight)
|
160
|
+
# end
|
161
|
+
|
162
|
+
# parents.each {|(p_node, _)| p_node.delete_child!(self) }
|
163
|
+
#end
|
164
|
+
|
165
|
+
#def delete_child!(node)
|
166
|
+
# self.children = self.children.reject {|c| c == node }
|
167
|
+
#end
|
168
|
+
|
169
|
+
# Force object_id to be the equality mechanism, rather than struct's
|
170
|
+
# default which delegates to == on each value. That is wrong because
|
171
|
+
# we want to be able to dup a node in the tree construction process and
|
172
|
+
# not have those compare equal to each other.
|
173
|
+
def ==(other)
|
174
|
+
object_id == other.object_id
|
175
|
+
end
|
176
|
+
|
177
|
+
def inspect
|
178
|
+
"#{frame_id}: #{name} - ##{samples}\n" +
|
179
|
+
" Parents: #{parents.map{ |(p, w)| "#{p.name}: #{w}"}.join("\n ") }\n" +
|
180
|
+
" Children: #{children_edges.inspect} \n"
|
181
|
+
end
|
182
|
+
|
183
|
+
#def all_descendants(max_depth=100)
|
184
|
+
# descendants = [self]
|
185
|
+
# unchecked_edge = self.children.reject(&:app?)
|
186
|
+
# stop = false
|
187
|
+
|
188
|
+
# puts "----------------------------------------"
|
189
|
+
|
190
|
+
# while max_depth > 0 && !stop
|
191
|
+
# before_count = descendants.length
|
192
|
+
|
193
|
+
# descendants = (descendants + unchecked_edge).uniq
|
194
|
+
# unchecked_edge = unchecked_edge.map(&:children).flatten.uniq.reject(&:app?)
|
195
|
+
# puts "UncheckedEdge Children: #{unchecked_edge.length}"
|
196
|
+
|
197
|
+
# after_count = descendants.length
|
198
|
+
# stop = true if before_count == after_count
|
199
|
+
# max_depth = max_depth - 1
|
200
|
+
# end
|
201
|
+
|
202
|
+
# puts "#{name} - Found #{descendants.length} children after #{100 - max_depth} iterations"
|
203
|
+
|
204
|
+
# puts "----------------------------------------"
|
205
|
+
|
206
|
+
# descendants
|
207
|
+
#end
|
208
|
+
|
209
|
+
def calls_only_app_nodes?
|
210
|
+
children.all?(&:app?)
|
211
|
+
end
|
212
|
+
|
213
|
+
def has_samples?
|
214
|
+
samples > 0
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
data/lib/scout_apm/store.rb
CHANGED
@@ -72,7 +72,7 @@ module ScoutApm
|
|
72
72
|
end
|
73
73
|
|
74
74
|
duration = Time.now - item.start_time
|
75
|
-
if last=stack.last
|
75
|
+
if last = stack.last
|
76
76
|
last.children_time += duration
|
77
77
|
end
|
78
78
|
|
@@ -90,8 +90,8 @@ module ScoutApm
|
|
90
90
|
|
91
91
|
# Uses controllers as the entry point for a transaction. Otherwise, stats are ignored.
|
92
92
|
if stack_empty and meta.metric_name.match(/\AController\//)
|
93
|
-
aggs=aggregate_calls(transaction_hash.dup,meta)
|
94
|
-
store_slow(options[:uri],transaction_hash.dup.merge(aggs),meta,stat)
|
93
|
+
aggs = aggregate_calls(transaction_hash.dup,meta)
|
94
|
+
store_slow(options[:uri], transaction_hash.dup.merge(aggs), meta, stat)
|
95
95
|
# deep duplicate
|
96
96
|
duplicate = aggs.dup
|
97
97
|
duplicate.each_pair do |k,v|
|
@@ -139,12 +139,20 @@ module ScoutApm
|
|
139
139
|
aggregates
|
140
140
|
end
|
141
141
|
|
142
|
+
SLOW_TRANSACTION_THRESHOLD = 2
|
143
|
+
|
142
144
|
# Stores slow transactions. This will be sent to the server.
|
143
|
-
def store_slow(uri,transaction_hash,parent_meta,parent_stat,options = {})
|
145
|
+
def store_slow(uri, transaction_hash, parent_meta, parent_stat, options = {})
|
144
146
|
@slow_transaction_lock.synchronize do
|
145
|
-
|
146
|
-
|
147
|
-
|
147
|
+
if parent_stat.total_call_time >= SLOW_TRANSACTION_THRESHOLD
|
148
|
+
slow_transaction = ScoutApm::SlowTransaction.new(uri,
|
149
|
+
parent_meta.metric_name,
|
150
|
+
parent_stat.total_call_time,
|
151
|
+
transaction_hash.dup,
|
152
|
+
ScoutApm::Context.current,
|
153
|
+
Thread::current[:scout_apm_trace_time],
|
154
|
+
Thread::current[:scout_apm_prof])
|
155
|
+
@slow_transactions.push(slow_transaction)
|
148
156
|
ScoutApm::Agent.instance.logger.debug "Slow transaction sample added. [URI: #{uri}] [Context: #{ScoutApm::Context.current.to_hash}] Array Size: #{@slow_transactions.size}"
|
149
157
|
end
|
150
158
|
end
|
data/lib/scout_apm/tracer.rb
CHANGED
@@ -54,13 +54,16 @@ module ScoutApm
|
|
54
54
|
elsif Thread::current[:scout_ignore_children]
|
55
55
|
return yield
|
56
56
|
end
|
57
|
+
|
57
58
|
if options.delete(:scope)
|
58
59
|
Thread::current[:scout_apm_sub_scope] = metric_name
|
59
60
|
end
|
61
|
+
|
60
62
|
if options[:ignore_children]
|
61
63
|
Thread::current[:scout_ignore_children] = true
|
62
64
|
end
|
63
65
|
stack_item = ScoutApm::Agent.instance.store.record(metric_name)
|
66
|
+
|
64
67
|
begin
|
65
68
|
yield
|
66
69
|
ensure
|
@@ -68,15 +71,21 @@ module ScoutApm
|
|
68
71
|
if options[:ignore_children]
|
69
72
|
Thread::current[:scout_ignore_children] = nil
|
70
73
|
end
|
74
|
+
|
71
75
|
ScoutApm::Agent.instance.store.stop_recording(stack_item,options)
|
72
76
|
end
|
73
77
|
end
|
74
78
|
|
75
|
-
def instrument_method(method,options = {})
|
79
|
+
def instrument_method(method, options = {})
|
76
80
|
ScoutApm::Agent.instance.logger.info "Instrumenting #{method}"
|
77
81
|
metric_name = options[:metric_name] || default_metric_name(method)
|
78
82
|
return if !instrumentable?(method) or instrumented?(method,metric_name)
|
79
|
-
|
83
|
+
|
84
|
+
class_eval(instrumented_method_string(
|
85
|
+
method,
|
86
|
+
{:metric_name => metric_name, :scope => options[:scope] }),
|
87
|
+
__FILE__, __LINE__
|
88
|
+
)
|
80
89
|
|
81
90
|
alias_method _uninstrumented_method_name(method, metric_name), method
|
82
91
|
alias_method method, _instrumented_method_name(method, metric_name)
|
@@ -86,12 +95,15 @@ module ScoutApm
|
|
86
95
|
|
87
96
|
def instrumented_method_string(method, options)
|
88
97
|
klass = (self === Module) ? "self" : "self.class"
|
89
|
-
"def #{_instrumented_method_name(method, options[:metric_name])}(*args, &block)
|
98
|
+
method_str = "def #{_instrumented_method_name(method, options[:metric_name])}(*args, &block)
|
90
99
|
result = #{klass}.instrument(\"#{options[:metric_name]}\",{:scope => #{options[:scope] || false}}) do
|
91
100
|
#{_uninstrumented_method_name(method, options[:metric_name])}(*args, &block)
|
92
101
|
end
|
93
102
|
result
|
94
103
|
end"
|
104
|
+
|
105
|
+
ScoutApm::Agent.instance.logger.debug "Instrumented Method:\n#{method_str}"
|
106
|
+
method_str
|
95
107
|
end
|
96
108
|
|
97
109
|
# The method must exist to be instrumented.
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# A fake implementation of stackprof, for systems that don't support it.
|
2
|
+
class StackProf
|
3
|
+
def self.start(*args)
|
4
|
+
@running = true
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.stop(*args)
|
8
|
+
@running = false
|
9
|
+
end
|
10
|
+
|
11
|
+
def running?
|
12
|
+
!!@running
|
13
|
+
end
|
14
|
+
|
15
|
+
def run(*args)
|
16
|
+
start
|
17
|
+
yield
|
18
|
+
stop
|
19
|
+
results
|
20
|
+
end
|
21
|
+
|
22
|
+
def sample(*args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def results(*args)
|
26
|
+
{
|
27
|
+
:version => 0.0,
|
28
|
+
:mode => :wall,
|
29
|
+
:interval => 1000,
|
30
|
+
:samples => 0,
|
31
|
+
:gc_samples => 0,
|
32
|
+
:missed_samples => 0,
|
33
|
+
:frames => {},
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
data/lib/scout_apm/version.rb
CHANGED
data/scout_apm.gemspec
CHANGED
@@ -18,6 +18,8 @@ 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
|
+
s.add_dependency 'stackprof'
|
22
|
+
|
21
23
|
s.add_development_dependency "minitest"
|
22
24
|
s.add_development_dependency "pry"
|
23
25
|
s.add_development_dependency "m"
|
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.18.stackprof4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Derek Haynes
|
@@ -11,6 +11,20 @@ bindir: bin
|
|
11
11
|
cert_chain: []
|
12
12
|
date: 2015-09-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: stackprof
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
14
28
|
- !ruby/object:Gem::Dependency
|
15
29
|
name: minitest
|
16
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -75,6 +89,10 @@ files:
|
|
75
89
|
- lib/scout_apm/capacity.rb
|
76
90
|
- lib/scout_apm/config.rb
|
77
91
|
- lib/scout_apm/context.rb
|
92
|
+
- lib/scout_apm/deploy_integrations/capistrano_2.cap
|
93
|
+
- lib/scout_apm/deploy_integrations/capistrano_2.rb
|
94
|
+
- lib/scout_apm/deploy_integrations/capistrano_3.cap
|
95
|
+
- lib/scout_apm/deploy_integrations/capistrano_3.rb
|
78
96
|
- lib/scout_apm/environment.rb
|
79
97
|
- lib/scout_apm/framework_integrations/rails_2.rb
|
80
98
|
- lib/scout_apm/framework_integrations/rails_3_or_4.rb
|
@@ -95,6 +113,7 @@ files:
|
|
95
113
|
- lib/scout_apm/metric_stats.rb
|
96
114
|
- lib/scout_apm/reporter.rb
|
97
115
|
- lib/scout_apm/serializers/app_server_load_serializer.rb
|
116
|
+
- lib/scout_apm/serializers/deploy_serializer.rb
|
98
117
|
- lib/scout_apm/serializers/directive_serializer.rb
|
99
118
|
- lib/scout_apm/serializers/payload_serializer.rb
|
100
119
|
- lib/scout_apm/server_integrations/null.rb
|
@@ -106,8 +125,10 @@ files:
|
|
106
125
|
- lib/scout_apm/server_integrations/webrick.rb
|
107
126
|
- lib/scout_apm/slow_transaction.rb
|
108
127
|
- lib/scout_apm/stack_item.rb
|
128
|
+
- lib/scout_apm/stackprof_tree_collapser.rb
|
109
129
|
- lib/scout_apm/store.rb
|
110
130
|
- lib/scout_apm/tracer.rb
|
131
|
+
- lib/scout_apm/utils/fake_stack_prof.rb
|
111
132
|
- lib/scout_apm/utils/installed_gems.rb
|
112
133
|
- lib/scout_apm/utils/null_logger.rb
|
113
134
|
- lib/scout_apm/utils/sql_sanitizer.rb
|
@@ -137,13 +158,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
158
|
version: '0'
|
138
159
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
160
|
requirements:
|
140
|
-
- - "
|
161
|
+
- - ">"
|
141
162
|
- !ruby/object:Gem::Version
|
142
|
-
version:
|
163
|
+
version: 1.3.1
|
143
164
|
requirements: []
|
144
165
|
rubyforge_project: scout_apm
|
145
166
|
rubygems_version: 2.2.2
|
146
167
|
signing_key:
|
147
168
|
specification_version: 4
|
148
169
|
summary: Ruby application performance monitoring
|
149
|
-
test_files:
|
170
|
+
test_files:
|
171
|
+
- test/data/config_test_1.yml
|
172
|
+
- test/test_helper.rb
|
173
|
+
- test/unit/config_test.rb
|
174
|
+
- test/unit/environment_test.rb
|
175
|
+
- test/unit/instruments/active_record_instruments_test.rb
|
176
|
+
- test/unit/sql_sanitizer_test.rb
|