scout_apm 0.1.17 → 0.1.18.stackprof4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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