scout_apm 2.0.0.pre → 2.0.0.pre2

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