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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d868630dc6de8b7e4387d2fc6e0e40860df1500d
4
- data.tar.gz: 585b6bbb87aeaeec7408cc24750fa3089165e4b8
3
+ metadata.gz: 7040acd30e2e30aceb89d48e43c699e007d87ea5
4
+ data.tar.gz: 76222f981eb632a50948c4f765ad387c671bbf80
5
5
  SHA512:
6
- metadata.gz: 0be21301d0033dbb09c11bfa79a6723c4ac24b900e99afab977152331a46dc7db8739813ae8d27f80b0fd65f04f02c5cd10f53fe4a0de89e8b3bfc7c1a6420e8
7
- data.tar.gz: 2e604678abf6cafcfd6a13ce39955212eae6486a69b92290427ad5f703ab4fa8556643388723dbe5f761409a039b49d8507bc730f1effc2af391216d85fb9a76
6
+ metadata.gz: 811ca92ad91009f8e3577caa8641a7b8c6e189bf2460b871f24c1d98d9e5a8b75376194391729aa42d47e3a47e9bf138dfcb34f0cd316f346281b870fd50e902
7
+ data.tar.gz: 5b0e5825e3d97e4ba7ce950792890bc5318be6e5fb23d60e6ad4c8bbeac6f1599a14d2f8ecf721c44cc56f046b711b090e322f8f87133dadf912220eeef7f613
data/CHANGELOG.markdown CHANGED
@@ -1,7 +1,3 @@
1
- # 0.1.17
2
-
3
- * Scrub sql strings for invalid encoding characters.
4
-
5
1
  # 0.1.16
6
2
 
7
3
  * Beta support for Sinatra monitoring.
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
@@ -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
@@ -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
- def value(key)
34
- value = ENV['SCOUT_'+key.upcase] || settings[key]
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 settings
49
- @settings ||= load_file
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
- if override_root = Agent.instance.config.value("application_root")
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, :metric_name => 'View/#{@template.virtual_path}/Rendering', :scope => true
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
@@ -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
- class ScoutApm::SlowTransaction
2
- BACKTRACE_THRESHOLD = 0.5 # the minimum threshold to record the backtrace for a metric.
3
- BACKTRACE_LIMIT = 5 # Max length of callers to display
4
- MAX_SIZE = 100 # Limits the size of the metric hash to prevent a metric explosion.
5
- attr_reader :metric_name, :total_call_time, :metrics, :meta, :uri, :context, :time, :prof, :raw_prof
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
- # Given a call stack, generates a filtered backtrace that:
8
- # * Limits to the app/models, app/controllers, or app/views directories
9
- # * Limits to 5 total callers
10
- # * Makes the app folder the top-level folder used in trace info
11
- def self.backtrace_parser(backtrace)
12
- stack = []
13
- backtrace.each do |c|
14
- if m=c.match(/(\/app\/(controllers|models|views)\/.+)/)
15
- stack << m[1]
16
- break if stack.size == BACKTRACE_LIMIT
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
- def initialize(uri,metric_name,total_call_time,metrics,context,time)
23
- @uri = uri
24
- @metric_name = metric_name
25
- @total_call_time = total_call_time
26
- @metrics = metrics
27
- @context = context
28
- @time = time
29
- end
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
- # Used to remove metrics when the payload will be too large.
32
- def clear_metrics!
33
- @metrics = nil
34
- self
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
@@ -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
- # tree map of all slow transactions
146
- if parent_stat.total_call_time >= 2
147
- @slow_transactions.push(ScoutApm::SlowTransaction.new(uri,parent_meta.metric_name,parent_stat.total_call_time,transaction_hash.dup,ScoutApm::Context.current,Thread::current[:scout_apm_trace_time]))
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
@@ -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
- class_eval instrumented_method_string(method, {:metric_name => metric_name, :scope => options[:scope]}), __FILE__, __LINE__
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
@@ -1,4 +1,4 @@
1
1
  module ScoutApm
2
- VERSION = "0.1.17"
2
+ VERSION = "0.1.18.stackprof4"
3
3
  end
4
4
 
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.17
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: '0'
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