scout_apm 2.0.0.pre → 2.0.0.pre2

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.markdown +22 -5
  4. data/Rakefile +5 -0
  5. data/lib/scout_apm.rb +4 -0
  6. data/lib/scout_apm/agent.rb +22 -8
  7. data/lib/scout_apm/agent/reporting.rb +8 -3
  8. data/lib/scout_apm/attribute_arranger.rb +4 -0
  9. data/lib/scout_apm/bucket_name_splitter.rb +3 -3
  10. data/lib/scout_apm/config.rb +5 -2
  11. data/lib/scout_apm/histogram.rb +20 -0
  12. data/lib/scout_apm/instant_reporting.rb +40 -0
  13. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +11 -1
  14. data/lib/scout_apm/instruments/percentile_sampler.rb +38 -0
  15. data/lib/scout_apm/layaway.rb +1 -4
  16. data/lib/scout_apm/layaway_file.rb +26 -2
  17. data/lib/scout_apm/layer.rb +1 -1
  18. data/lib/scout_apm/layer_converters/converter_base.rb +6 -4
  19. data/lib/scout_apm/layer_converters/slow_job_converter.rb +21 -13
  20. data/lib/scout_apm/layer_converters/slow_request_converter.rb +37 -24
  21. data/lib/scout_apm/metric_meta.rb +5 -1
  22. data/lib/scout_apm/metric_set.rb +15 -6
  23. data/lib/scout_apm/reporter.rb +9 -3
  24. data/lib/scout_apm/request_histograms.rb +46 -0
  25. data/lib/scout_apm/scored_item_set.rb +79 -0
  26. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +2 -0
  27. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +2 -0
  28. data/lib/scout_apm/slow_job_policy.rb +89 -19
  29. data/lib/scout_apm/slow_job_record.rb +18 -1
  30. data/lib/scout_apm/slow_request_policy.rb +80 -12
  31. data/lib/scout_apm/slow_transaction.rb +22 -3
  32. data/lib/scout_apm/store.rb +35 -13
  33. data/lib/scout_apm/tracked_request.rb +63 -11
  34. data/lib/scout_apm/utils/backtrace_parser.rb +4 -4
  35. data/lib/scout_apm/utils/sql_sanitizer.rb +1 -1
  36. data/lib/scout_apm/utils/sql_sanitizer_regex.rb +2 -2
  37. data/lib/scout_apm/utils/sql_sanitizer_regex_1_8_7.rb +2 -2
  38. data/lib/scout_apm/version.rb +1 -1
  39. data/scout_apm.gemspec +1 -0
  40. data/test/test_helper.rb +4 -3
  41. data/test/unit/layaway_test.rb +5 -8
  42. data/test/unit/metric_set_test.rb +101 -0
  43. data/test/unit/scored_item_set_test.rb +65 -0
  44. data/test/unit/serializers/payload_serializer_test.rb +2 -1
  45. data/test/unit/slow_item_set_test.rb +2 -1
  46. data/test/unit/slow_request_policy_test.rb +42 -0
  47. data/test/unit/sql_sanitizer_test.rb +6 -0
  48. metadata +28 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b18724905e7c795caad3fc416688b3044e624d79
4
- data.tar.gz: ee4ec99aeb9fa9e32c7b846caec0f010070546c2
3
+ metadata.gz: 119d6658eaefd7e3b177c1e982006f0d59dd208b
4
+ data.tar.gz: 5585bca449804241d8778a1ff716444c50ac86d8
5
5
  SHA512:
6
- metadata.gz: 236bfd93df93733c409684ac735dfbc77b5412f221127b5acbec9fc679b3141c6ac8e4c46c485295532aee7821b8fda36edd648233f820ed045d667905237448
7
- data.tar.gz: 4b43e6158568bd089b3174bd8af445baef2260f92a33c31b50ed8627722a9f194fc2e4352c509c12e677e71f824e04e22226ce5c1b7a5c542f2e806167c0aabb
6
+ metadata.gz: 5e6ad957c5948b239d530e9cc4e35445a37abb1331ef3c20357371a1c5da9286e4b9d38e84df2aecedd23a39e1a5d0163d76dd76549e41c700b78e15f770338d
7
+ data.tar.gz: 060b1d7a09e6e5c37cd1a087bd7bf13faaab3e3b5b3aaf8d7f4173a50fa97c76d39c4ccaf299b05cdd48232a6b7638caeab88674110072a26692ad77fad2533e
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ test/tmp/*
15
15
  .DS_Store
16
16
  test/tmp/*coverage/*
17
17
  coverage/*
18
+ lib/allocations.bundle
@@ -1,17 +1,34 @@
1
+
1
2
  # 2.0.0
2
3
 
3
4
  * Reporting object allocation metrics and mem delta with slow requests and jobs.
5
+ * Collecting memory metrics on slow transactions
6
+ * Collecting additional fields for slow transactions:
7
+ * hostname
8
+ * seconds_since_startup (larger memory increases and other other odd behavior more common when close to startup)
9
+ * Initial support for instant traces
10
+ * Collect 95th percentiles
11
+
12
+ # 1.5.5
13
+
14
+ * Handle backslash escaped quotes inside mysql strings.
15
+
16
+ # 1.5.4
17
+
18
+ * Fix issue where error counts were being misreported
19
+ * Politely ignore cases when `request.remote_ip` raises exceptions.
20
+
21
+ # 1.5.3
22
+
23
+ * Fix another minor bug related to iso8601 timestamps
4
24
 
5
25
  # 1.5.2
6
26
 
7
- * Fix deploy webhook endpoint
27
+ * Force timestamps to be iso8601 format
8
28
 
9
29
  # 1.5.1
10
30
 
11
- * Collecting memory metrics on slow transactions
12
- * Collecting additional fields for slow transactions:
13
- * hostname
14
- * seconds_since_startup (larger memory increases and other other odd behavior more common when close to startup)
31
+ * Add `ignore_traces` config option to ignore SlowTransactions from certain URIs.
15
32
 
16
33
  # 1.5.0
17
34
 
data/Rakefile CHANGED
@@ -17,3 +17,8 @@ task :console do
17
17
  ARGV.clear
18
18
  IRB.start
19
19
  end
20
+
21
+ # Rake Compiler
22
+ require 'rake/extensiontask'
23
+ Rake::ExtensionTask.new('allocations')
24
+
@@ -87,6 +87,7 @@ require 'scout_apm/instruments/rails_router'
87
87
  require 'scout_apm/instruments/sinatra'
88
88
  require 'scout_apm/instruments/process/process_cpu'
89
89
  require 'scout_apm/instruments/process/process_memory'
90
+ require 'scout_apm/instruments/percentile_sampler'
90
91
  require 'allocations'
91
92
 
92
93
  require 'scout_apm/app_server_load'
@@ -116,15 +117,18 @@ require 'scout_apm/store'
116
117
  require 'scout_apm/tracer'
117
118
  require 'scout_apm/context'
118
119
  require 'scout_apm/stackprof_tree_collapser'
120
+ require 'scout_apm/instant_reporting'
119
121
 
120
122
  require 'scout_apm/metric_meta'
121
123
  require 'scout_apm/metric_stats'
122
124
  require 'scout_apm/slow_transaction'
123
125
  require 'scout_apm/slow_job_record'
124
126
  require 'scout_apm/slow_item_set'
127
+ require 'scout_apm/scored_item_set'
125
128
  require 'scout_apm/slow_request_policy'
126
129
  require 'scout_apm/slow_job_policy'
127
130
  require 'scout_apm/job_record'
131
+ require 'scout_apm/request_histograms'
128
132
 
129
133
  require 'scout_apm/capacity'
130
134
  require 'scout_apm/attribute_arranger'
@@ -21,6 +21,12 @@ module ScoutApm
21
21
  attr_reader :slow_job_policy
22
22
  attr_reader :process_start_time # used when creating slow transactions to report how far from startup the transaction was recorded.
23
23
 
24
+ # Histogram of the cumulative requests since the start of the process
25
+ attr_reader :request_histograms
26
+
27
+ # Histogram of the requests since last reset. Reset by the sampler, so once per minutes.
28
+ attr_reader :request_histograms_resettable
29
+
24
30
  # All access to the agent is thru this class method to ensure multiple Agent instances are not initialized per-Ruby process.
25
31
  def self.instance(options = {})
26
32
  @@instance ||= self.new(options)
@@ -41,14 +47,15 @@ module ScoutApm
41
47
  # Later in initialization, we reset @config to include the file.
42
48
  @config = ScoutApm::Config.without_file
43
49
 
50
+ @slow_request_policy = ScoutApm::SlowRequestPolicy.new
44
51
  @slow_job_policy = ScoutApm::SlowJobPolicy.new
52
+ @request_histograms = ScoutApm::RequestHistograms.new
53
+ @request_histograms_resettable = ScoutApm::RequestHistograms.new
45
54
 
46
55
  @store = ScoutApm::Store.new
47
56
  @layaway = ScoutApm::Layaway.new
48
57
  @metric_lookup = Hash.new
49
58
 
50
- @slow_request_policy = ScoutApm::SlowRequestPolicy.new
51
-
52
59
  @capacity = ScoutApm::Capacity.new
53
60
  @installed_instruments = []
54
61
  end
@@ -119,7 +126,8 @@ module ScoutApm
119
126
 
120
127
  @samplers = [
121
128
  ScoutApm::Instruments::Process::ProcessCpu.new(environment.processors, logger),
122
- ScoutApm::Instruments::Process::ProcessMemory.new(logger)
129
+ ScoutApm::Instruments::Process::ProcessMemory.new(logger),
130
+ ScoutApm::Instruments::PercentileSampler.new(logger, 95),
123
131
  ]
124
132
 
125
133
  app_server_load_hook
@@ -193,9 +201,11 @@ module ScoutApm
193
201
  store.write_to_layaway(layaway, :force)
194
202
  end
195
203
 
196
- # Make sure we don't exit the process while the background worker is running its task.
197
- logger.debug "Joining background worker thread"
198
- @background_worker_thread.join if @background_worker_thread
204
+ # make sure we don't exit the process while the background worker is running its task. Bypass this step in development.
205
+ if environment.env != 'development'
206
+ logger.debug "Joining background worker thread"
207
+ @background_worker_thread.join if @background_worker_thread
208
+ end
199
209
  end
200
210
 
201
211
  def started?
@@ -308,8 +318,12 @@ module ScoutApm
308
318
  def run_samplers
309
319
  @samplers.each do |sampler|
310
320
  begin
311
- result = sampler.run
312
- store.track_one!(sampler.metric_type, sampler.metric_name, result) if result
321
+ if sampler.respond_to? :metrics
322
+ store.track!(sampler.metrics)
323
+ else
324
+ result = sampler.run
325
+ store.track_one!(sampler.metric_type, sampler.metric_name, result) if result
326
+ end
313
327
  rescue => e
314
328
  logger.info "Error reading #{sampler.human_name}"
315
329
  logger.debug e.message
@@ -50,7 +50,7 @@ module ScoutApm
50
50
  :platform => "ruby",
51
51
  }
52
52
 
53
- log_deliver(metrics, slow_transactions, metadata)
53
+ log_deliver(metrics, slow_transactions, metadata, slow_jobs)
54
54
 
55
55
  payload = ScoutApm::Serializers::PayloadSerializer.serialize(metadata, metrics, slow_transactions, jobs, slow_jobs)
56
56
  logger.debug("Payload: #{payload}")
@@ -62,7 +62,7 @@ module ScoutApm
62
62
  logger.debug e.backtrace
63
63
  end
64
64
 
65
- def log_deliver(metrics, slow_transactions, metadata)
65
+ def log_deliver(metrics, slow_transactions, metadata, jobs_traces)
66
66
  total_request_count = metrics.
67
67
  select { |meta,stats| meta.metric_name =~ /\AController/ }.
68
68
  inject(0) {|sum, (_, stat)| sum + stat.call_count }
@@ -75,7 +75,12 @@ module ScoutApm
75
75
  "Recorded across (unknown) processes"
76
76
  end
77
77
 
78
- logger.info "[#{Time.parse(metadata[:agent_time]).strftime("%H:%M")}] Delivering #{metrics.length} Metrics for #{total_request_count} requests and #{slow_transactions.length} Slow Transaction Traces, #{process_log_str}."
78
+ time_clause = "[#{Time.parse(metadata[:agent_time]).strftime("%H:%M")}]"
79
+ metrics_clause = "#{metrics.length} Metrics for #{total_request_count} requests"
80
+ slow_trans_clause = "#{slow_transactions.length} Slow Transaction Traces"
81
+ job_clause = "#{jobs_traces.length} Job Traces"
82
+
83
+ logger.info "#{time_clause} Delivering #{metrics_clause} and #{slow_trans_clause} and #{job_clause}, #{process_log_str}."
79
84
  logger.debug("Metrics: #{metrics.pretty_inspect}\nSlowTrans: #{slow_transactions.pretty_inspect}\nMetadata: #{metadata.inspect.pretty_inspect}")
80
85
  end
81
86
 
@@ -7,6 +7,10 @@ module ScoutApm
7
7
  case attribute
8
8
  when Array
9
9
  attribute_hash[attribute[0]] = subject.send(attribute[1])
10
+ when :bucket
11
+ attribute_hash[attribute] = subject.bucket_type
12
+ when :name
13
+ attribute_hash[attribute] = subject.bucket_name
10
14
  when Symbol
11
15
  attribute_hash[attribute] = subject.send(attribute)
12
16
  end
@@ -1,15 +1,15 @@
1
1
  module ScoutApm
2
2
  module BucketNameSplitter
3
- def bucket
3
+ def bucket_type
4
4
  split_metric_name(metric_name).first
5
5
  end
6
6
 
7
- def name
7
+ def bucket_name
8
8
  split_metric_name(metric_name).last
9
9
  end
10
10
 
11
11
  def key
12
- {:bucket => bucket, :name => name}
12
+ {:bucket => bucket_type, :name => bucket_name}
13
13
  end
14
14
 
15
15
  private
@@ -7,14 +7,15 @@ require 'scout_apm/environment'
7
7
  #
8
8
  # application_root - override the detected directory of the application
9
9
  # data_file - override the default temporary storage location. Must be a location in a writable directory
10
- # hostname - override the default hostname detection. Default varies by environment - either system hostname, or PAAS hostname
10
+ # host - override the default hostname detection. Default varies by environment - either system hostname, or PAAS hostname
11
+ # direct_host - override the default "direct" host. The direct_host bypasses the ingestion pipeline and goes directly to the webserver, and is primarily used for features under development.
11
12
  # key - the account key with Scout APM. Found in Settings in the Web UI
12
13
  # log_file_path - either a directory or "STDOUT".
13
14
  # log_level - DEBUG / INFO / WARN as usual
14
15
  # monitor - true or false. False prevents any instrumentation from starting
15
16
  # name - override the name reported to APM. This is the name that shows in the Web UI
16
17
  # uri_reporting - 'path' or 'full_path' default is 'full_path', which reports URL params as well as the path.
17
- # report_format - 'json' or 'marshal'. Json is default, Marshal is deprecated
18
+ # report_format - 'json' or 'marshal'. Marshal is legacy and will be removed.
18
19
  #
19
20
  # Any of these config settings can be set with an environment variable prefixed
20
21
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
@@ -65,12 +66,14 @@ module ScoutApm
65
66
  class ConfigDefaults
66
67
  DEFAULTS = {
67
68
  'host' => 'https://checkin.scoutapp.com',
69
+ 'direct_host' => 'https://apm.scoutapp.com',
68
70
  'log_level' => 'info',
69
71
  'stackprof_interval' => 20000, # microseconds, 1000 = 1 millisecond, so 20k == 20 milliseconds
70
72
  'uri_reporting' => 'full_path',
71
73
  'report_format' => 'json',
72
74
  'disabled_instruments' => [],
73
75
  'enable_background_jobs' => true,
76
+ 'ignore_traces' => [],
74
77
  }.freeze
75
78
 
76
79
  def value(key)
@@ -56,6 +56,26 @@ module ScoutApm
56
56
  end
57
57
  end
58
58
 
59
+ # Given a value, where in this histogram does it fall?
60
+ # Returns a float between 0 and 1
61
+ def approximate_quantile_of_value(v)
62
+ mutex.synchronize do
63
+ return 100 if total == 0
64
+
65
+ count_examined = 0
66
+
67
+ bins.each_with_index do |bin, index|
68
+ if v <= bin.value
69
+ break
70
+ end
71
+
72
+ count_examined += bin.count
73
+ end
74
+
75
+ count_examined / total.to_f
76
+ end
77
+ end
78
+
59
79
  def mean
60
80
  mutex.synchronize do
61
81
  if total == 0
@@ -0,0 +1,40 @@
1
+ # InstantReporting is used when a specially flagged request hits the application.
2
+ # The agent traces the request regardless of its performance characteristics, and reports it immediately to our servers, for instant feedback.
3
+ # The request is detected in the ActionController instrumentation, and flagged in TrackedRequest. The trace is prepped here, and handed off to a Reporter for the actual POST.
4
+ module ScoutApm
5
+ class InstantReporting
6
+ # trace is an instance of SlowTransaction
7
+ # instant_key is what was passed in from the browser to trigger the instant trace
8
+ def initialize(trace, instant_key)
9
+ @trace = trace
10
+ @instant_key = instant_key
11
+ end
12
+
13
+ def call
14
+ Thread.new do
15
+ # Serialize that trace. We reuse the PayloadSerializer, but only provide the metadata and traces.
16
+ # In this case, the traces array will always have just one element.
17
+ metadata = {
18
+ :app_root => ScoutApm::Environment.instance.root.to_s,
19
+ :unique_id => ScoutApm::Utils::UniqueId.simple,
20
+ :agent_version => ScoutApm::VERSION,
21
+ :agent_time => Time.now.iso8601,
22
+ :agent_pid => Process.pid,
23
+ :platform => "ruby",
24
+ }
25
+
26
+ metrics = []
27
+ traces = [@trace]
28
+ jobs = []
29
+ slow_jobs = []
30
+
31
+ payload = ScoutApm::Serializers::PayloadSerializer.serialize(metadata, metrics, traces, jobs, slow_jobs)
32
+
33
+ # Hand it off to the reporter for POST to our servers
34
+ reporter = Reporter.new(:instant_trace, config = Agent.instance.config, logger = Agent.instance.logger, instant_key = @instant_key)
35
+ reporter.report(payload, {'Content-Type' => 'application/json'} )
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -61,8 +61,18 @@ module ScoutApm
61
61
  req = ScoutApm::RequestManager.lookup
62
62
  path = ScoutApm::Agent.instance.config.value("uri_reporting") == 'path' ? request.path : request.fullpath
63
63
  req.annotate_request(:uri => path)
64
- req.context.add_user(:ip => request.remote_ip)
64
+
65
+ # IP Spoofing Protection can throw an exception, just move on w/o remote ip
66
+ req.context.add_user(:ip => request.remote_ip) rescue nil
67
+
65
68
  req.set_headers(request.headers)
69
+
70
+ # Check if this this request is to be reported instantly
71
+ if instant_key = request.cookies['scoutapminstant']
72
+ Agent.instance.logger.info "Instant trace request with key=#{instant_key} for path=#{path}"
73
+ req.instant_key = instant_key
74
+ end
75
+
66
76
  req.web!
67
77
 
68
78
  req.start_layer( ScoutApm::Layer.new("Controller", "#{controller_path}/#{action_name}") )
@@ -0,0 +1,38 @@
1
+ module ScoutApm
2
+ module Instruments
3
+ class PercentileSampler
4
+ attr_reader :logger
5
+
6
+ attr_reader :percentiles
7
+
8
+ def initialize(logger, percentiles)
9
+ @logger = logger
10
+ @percentiles = Array(percentiles)
11
+ end
12
+
13
+ def human_name
14
+ "Percentiles"
15
+ end
16
+
17
+ def metrics
18
+ ms = {}
19
+
20
+ ScoutApm::Agent.instance.request_histograms_resettable.each_name do |name|
21
+ percentiles.each do |percentile|
22
+ meta = MetricMeta.new("Percentile/#{percentile}/#{name}")
23
+ stat = MetricStats.new
24
+ stat.update!(ScoutApm::Agent.instance.request_histograms_resettable.quantile(name, percentile))
25
+ ms[meta] = stat
26
+ end
27
+ end
28
+
29
+ # Wipe the histograms, get ready for the next minute's worth of data.
30
+ ScoutApm::Agent.instance.request_histograms_resettable.reset_all!
31
+
32
+ ms
33
+ end
34
+
35
+ private
36
+ end
37
+ end
38
+ end
@@ -18,10 +18,7 @@ module ScoutApm
18
18
  new_req = new_val.request_count
19
19
  ScoutApm::Agent.instance.logger.debug("Merging Two reporting periods (#{old_val.timestamp.to_s}, #{new_val.timestamp.to_s}): old req #{old_req}, new req #{new_req}")
20
20
 
21
- old_val.
22
- merge_metrics!(new_val.metrics_payload).
23
- merge_slow_transactions!(new_val.slow_transactions).
24
- merge_jobs!(new_val.jobs)
21
+ old_val.merge(new_val)
25
22
  }
26
23
 
27
24
  ScoutApm::Agent.instance.logger.debug("AddReportingPeriod: AfterMerge Timestamps: #{existing_data.keys.map(&:to_s).inspect}")
@@ -2,8 +2,32 @@
2
2
  module ScoutApm
3
3
  class LayawayFile
4
4
  def path
5
- ScoutApm::Agent.instance.config.value("data_file") ||
6
- "#{ScoutApm::Agent.instance.default_log_path}/scout_apm.db"
5
+ return @path if @path
6
+
7
+ candidates = [
8
+ ScoutApm::Agent.instance.config.value("data_file"),
9
+ "#{ScoutApm::Agent.instance.default_log_path}/scout_apm.db",
10
+ "#{ScoutApm::Agent.instance.environment.root}/tmp/scout_apm.db"
11
+ ]
12
+
13
+ candidates.each do |candidate|
14
+ next if candidate.nil?
15
+
16
+ begin
17
+ ScoutApm::Agent.instance.logger.debug("Checking Layaway File Location: #{candidate}")
18
+ File.open(candidate, "w") { |f| } # Open & Close to check that we can
19
+
20
+ # No exception, it is valid
21
+ ScoutApm::Agent.instance.logger.info("Layaway File location found: #{candidate}")
22
+ @path = candidate
23
+ return @path
24
+ rescue Exception
25
+ ScoutApm::Agent.instance.logger.debug("Couldn't open layaway file for test write at #{candidate}")
26
+ end
27
+ end
28
+
29
+ ScoutApm::Agent.instance.logger.error("No valid layaway file found, please set a location in the configuration key `data_file`")
30
+ nil
7
31
  end
8
32
 
9
33
  def dump(object)
@@ -40,7 +40,7 @@ module ScoutApm
40
40
  # :class_name - The ActiveRecord class name (From notification instantiation.active_record)
41
41
  attr_reader :annotations
42
42
 
43
- BACKTRACE_CALLER_LIMIT = 30 # maximum number of lines to send thru for backtrace analysis
43
+ BACKTRACE_CALLER_LIMIT = 50 # maximum number of lines to send thru for backtrace analysis
44
44
 
45
45
  def initialize(type, name, start_time = Time.now)
46
46
  @type = type