scout_apm 2.2.0.pre3 → 2.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/CHANGELOG.markdown +147 -2
  4. data/Guardfile +43 -0
  5. data/Rakefile +2 -2
  6. data/ext/allocations/allocations.c +6 -0
  7. data/ext/allocations/extconf.rb +1 -0
  8. data/ext/rusage/README.md +26 -0
  9. data/ext/rusage/extconf.rb +5 -0
  10. data/ext/rusage/rusage.c +52 -0
  11. data/lib/scout_apm.rb +28 -15
  12. data/lib/scout_apm/agent.rb +89 -37
  13. data/lib/scout_apm/agent/logging.rb +6 -1
  14. data/lib/scout_apm/agent/reporting.rb +9 -6
  15. data/lib/scout_apm/app_server_load.rb +21 -10
  16. data/lib/scout_apm/attribute_arranger.rb +6 -3
  17. data/lib/scout_apm/background_job_integrations/delayed_job.rb +71 -1
  18. data/lib/scout_apm/background_job_integrations/resque.rb +85 -0
  19. data/lib/scout_apm/background_job_integrations/sidekiq.rb +22 -20
  20. data/lib/scout_apm/background_recorder.rb +43 -0
  21. data/lib/scout_apm/background_worker.rb +19 -15
  22. data/lib/scout_apm/config.rb +138 -28
  23. data/lib/scout_apm/db_query_metric_set.rb +80 -0
  24. data/lib/scout_apm/db_query_metric_stats.rb +102 -0
  25. data/lib/scout_apm/debug.rb +37 -0
  26. data/lib/scout_apm/environment.rb +22 -15
  27. data/lib/scout_apm/git_revision.rb +51 -0
  28. data/lib/scout_apm/histogram.rb +11 -2
  29. data/lib/scout_apm/instant/assets/xmlhttp_instrumentation.html +2 -2
  30. data/lib/scout_apm/instant/middleware.rb +196 -54
  31. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +89 -68
  32. data/lib/scout_apm/instruments/action_view.rb +49 -0
  33. data/lib/scout_apm/instruments/active_record.rb +127 -3
  34. data/lib/scout_apm/instruments/grape.rb +4 -3
  35. data/lib/scout_apm/instruments/middleware_detailed.rb +4 -6
  36. data/lib/scout_apm/instruments/mongoid.rb +24 -3
  37. data/lib/scout_apm/instruments/net_http.rb +7 -2
  38. data/lib/scout_apm/instruments/percentile_sampler.rb +36 -19
  39. data/lib/scout_apm/instruments/process/process_cpu.rb +3 -2
  40. data/lib/scout_apm/instruments/process/process_memory.rb +3 -3
  41. data/lib/scout_apm/instruments/resque.rb +40 -0
  42. data/lib/scout_apm/layaway.rb +67 -28
  43. data/lib/scout_apm/layer.rb +19 -59
  44. data/lib/scout_apm/layer_children_set.rb +77 -0
  45. data/lib/scout_apm/layer_converters/allocation_metric_converter.rb +5 -6
  46. data/lib/scout_apm/layer_converters/converter_base.rb +201 -14
  47. data/lib/scout_apm/layer_converters/database_converter.rb +55 -0
  48. data/lib/scout_apm/layer_converters/depth_first_walker.rb +22 -10
  49. data/lib/scout_apm/layer_converters/error_converter.rb +5 -7
  50. data/lib/scout_apm/layer_converters/find_layer_by_type.rb +34 -0
  51. data/lib/scout_apm/layer_converters/histograms.rb +14 -0
  52. data/lib/scout_apm/layer_converters/job_converter.rb +36 -50
  53. data/lib/scout_apm/layer_converters/metric_converter.rb +17 -19
  54. data/lib/scout_apm/layer_converters/request_queue_time_converter.rb +10 -12
  55. data/lib/scout_apm/layer_converters/slow_job_converter.rb +41 -115
  56. data/lib/scout_apm/layer_converters/slow_request_converter.rb +33 -117
  57. data/lib/scout_apm/limited_layer.rb +126 -0
  58. data/lib/scout_apm/metric_meta.rb +0 -5
  59. data/lib/scout_apm/metric_set.rb +9 -1
  60. data/lib/scout_apm/metric_stats.rb +7 -8
  61. data/lib/scout_apm/rack.rb +26 -0
  62. data/lib/scout_apm/remote/message.rb +23 -0
  63. data/lib/scout_apm/remote/recorder.rb +57 -0
  64. data/lib/scout_apm/remote/router.rb +49 -0
  65. data/lib/scout_apm/remote/server.rb +58 -0
  66. data/lib/scout_apm/reporter.rb +51 -15
  67. data/lib/scout_apm/request_histograms.rb +4 -0
  68. data/lib/scout_apm/request_manager.rb +2 -1
  69. data/lib/scout_apm/scored_item_set.rb +7 -0
  70. data/lib/scout_apm/serializers/db_query_serializer_to_json.rb +15 -0
  71. data/lib/scout_apm/serializers/histograms_serializer_to_json.rb +21 -0
  72. data/lib/scout_apm/serializers/payload_serializer.rb +10 -3
  73. data/lib/scout_apm/serializers/payload_serializer_to_json.rb +6 -6
  74. data/lib/scout_apm/serializers/slow_jobs_serializer_to_json.rb +2 -1
  75. data/lib/scout_apm/server_integrations/puma.rb +5 -2
  76. data/lib/scout_apm/slow_job_policy.rb +1 -10
  77. data/lib/scout_apm/slow_job_record.rb +6 -1
  78. data/lib/scout_apm/slow_request_policy.rb +1 -10
  79. data/lib/scout_apm/slow_transaction.rb +20 -2
  80. data/lib/scout_apm/store.rb +66 -12
  81. data/lib/scout_apm/synchronous_recorder.rb +26 -0
  82. data/lib/scout_apm/tracked_request.rb +136 -71
  83. data/lib/scout_apm/utils/active_record_metric_name.rb +8 -4
  84. data/lib/scout_apm/utils/backtrace_parser.rb +3 -3
  85. data/lib/scout_apm/utils/gzip_helper.rb +24 -0
  86. data/lib/scout_apm/utils/numbers.rb +14 -0
  87. data/lib/scout_apm/utils/scm.rb +14 -0
  88. data/lib/scout_apm/version.rb +1 -1
  89. data/scout_apm.gemspec +5 -4
  90. data/test/test_helper.rb +18 -0
  91. data/test/unit/config_test.rb +59 -8
  92. data/test/unit/db_query_metric_set_test.rb +56 -0
  93. data/test/unit/db_query_metric_stats_test.rb +113 -0
  94. data/test/unit/git_revision_test.rb +15 -0
  95. data/test/unit/histogram_test.rb +14 -0
  96. data/test/unit/instruments/net_http_test.rb +21 -0
  97. data/test/unit/instruments/percentile_sampler_test.rb +137 -0
  98. data/test/unit/layaway_test.rb +20 -0
  99. data/test/unit/layer_children_set_test.rb +88 -0
  100. data/test/unit/layer_converters/depth_first_walker_test.rb +66 -0
  101. data/test/unit/layer_converters/metric_converter_test.rb +22 -0
  102. data/test/unit/layer_converters/stubs.rb +33 -0
  103. data/test/unit/limited_layer_test.rb +53 -0
  104. data/test/unit/remote/test_message.rb +13 -0
  105. data/test/unit/remote/test_router.rb +33 -0
  106. data/test/unit/remote/test_server.rb +15 -0
  107. data/test/unit/serializers/payload_serializer_test.rb +3 -12
  108. data/test/unit/store_test.rb +66 -0
  109. data/test/unit/test_tracked_request.rb +87 -0
  110. data/test/unit/utils/active_record_metric_name_test.rb +8 -0
  111. data/test/unit/utils/backtrace_parser_test.rb +5 -0
  112. data/test/unit/utils/numbers_test.rb +15 -0
  113. data/test/unit/utils/scm.rb +17 -0
  114. metadata +125 -30
  115. data/ext/stacks/extconf.rb +0 -37
  116. data/ext/stacks/scout_atomics.h +0 -86
  117. data/ext/stacks/stacks.c +0 -811
  118. data/lib/scout_apm/capacity.rb +0 -57
  119. data/lib/scout_apm/deploy_integrations/capistrano_2.cap +0 -12
  120. data/lib/scout_apm/deploy_integrations/capistrano_2.rb +0 -83
  121. data/lib/scout_apm/deploy_integrations/capistrano_3.cap +0 -12
  122. data/lib/scout_apm/deploy_integrations/capistrano_3.rb +0 -88
  123. data/lib/scout_apm/instruments/delayed_job.rb +0 -57
  124. data/lib/scout_apm/serializers/deploy_serializer.rb +0 -16
  125. data/lib/scout_apm/trace_compactor.rb +0 -312
  126. data/lib/scout_apm/utils/fake_stacks.rb +0 -87
  127. data/tester.rb +0 -53
@@ -0,0 +1,80 @@
1
+ module ScoutApm
2
+ class DbQueryMetricSet
3
+ include Enumerable
4
+
5
+ attr_reader :metrics # the raw metrics. You probably want #metrics_to_report
6
+ attr_reader :config # A ScoutApm::Config instance
7
+
8
+ def initialize(config=ScoutApm::Agent.instance.config)
9
+ # A hash of DbQueryMetricStats values, keyed by DbQueryMetricStats.key
10
+ @metrics = Hash.new
11
+ @config = config
12
+ end
13
+
14
+ def each
15
+ metrics.each do |_key, db_query_metric_stat|
16
+ yield db_query_metric_stat
17
+ end
18
+ end
19
+
20
+ # Looks up a DbQueryMetricStats instance in the +@metrics+ hash. Sets the value to +other+ if no key
21
+ # Returns a DbQueryMetricStats instance
22
+ def lookup(other)
23
+ metrics[other.key] ||= other
24
+ end
25
+
26
+ # Take another set, and merge it with this one
27
+ def combine!(other)
28
+ other.each do |metric|
29
+ self << metric
30
+ end
31
+ self
32
+ end
33
+
34
+ # Add a single DbQueryMetricStats object to this set.
35
+ #
36
+ # Looks up an existing one under this key and merges, or just saves a new
37
+ # one under the key
38
+ def <<(stat)
39
+ existing_stat = metrics[stat.key]
40
+ if existing_stat
41
+ existing_stat.combine!(stat)
42
+ elsif at_limit?
43
+ # We're full up, can't add any more.
44
+ # Should I log this? It may get super noisy?
45
+ else
46
+ metrics[stat.key] = stat
47
+ end
48
+ end
49
+
50
+ def increment_transaction_count!
51
+ metrics.each do |_key, db_query_metric_stat|
52
+ db_query_metric_stat.increment_transaction_count!
53
+ end
54
+ end
55
+
56
+ def metrics_to_report
57
+ report_limit = config.value('database_metric_report_limit')
58
+ if metrics.size > report_limit
59
+ metrics.
60
+ values.
61
+ sort_by {|stat| stat.call_time }.
62
+ reverse.
63
+ take(report_limit)
64
+ else
65
+ metrics.values
66
+ end
67
+ end
68
+
69
+ def inspect
70
+ metrics.map {|key, metric|
71
+ "#{key.inspect} - Count: #{metric.call_count}, Total Time: #{"%.2f" % metric.call_time}"
72
+ }.join("\n")
73
+ end
74
+
75
+ def at_limit?
76
+ @limit ||= config.value('database_metric_limit')
77
+ metrics.size >= @limit
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,102 @@
1
+ module ScoutApm
2
+ class DbQueryMetricStats
3
+
4
+ DEFAULT_HISTOGRAM_SIZE = 50
5
+
6
+ attr_reader :model_name
7
+ attr_reader :operation
8
+ attr_reader :scope
9
+
10
+ attr_reader :transaction_count
11
+
12
+ attr_reader :call_count
13
+ attr_reader :call_time
14
+ attr_reader :rows_returned
15
+
16
+ attr_reader :min_call_time
17
+ attr_reader :max_call_time
18
+
19
+ attr_reader :min_rows_returned
20
+ attr_reader :max_rows_returned
21
+
22
+ attr_reader :histogram
23
+
24
+ def initialize(model_name, operation, scope, call_count, call_time, rows_returned)
25
+ @model_name = model_name
26
+ @operation = operation
27
+
28
+ @call_count = call_count
29
+
30
+ @call_time = call_time
31
+ @min_call_time = call_time
32
+ @max_call_time = call_time
33
+
34
+ @rows_returned = rows_returned
35
+ @min_rows_returned = rows_returned
36
+ @max_rows_returned = rows_returned
37
+
38
+ # Should we have a histogram for timing, and one for rows_returned?
39
+ # This histogram is for call_time
40
+ @histogram = NumericHistogram.new(DEFAULT_HISTOGRAM_SIZE)
41
+ @histogram.add(call_time)
42
+
43
+ @transaction_count = 0
44
+
45
+ @scope = scope
46
+ end
47
+
48
+ # Merge data in this scope. Used in DbQueryMetricSet
49
+ def key
50
+ @key ||= [model_name, operation, scope]
51
+ end
52
+
53
+ # Combine data from another DbQueryMetricStats into +self+. Modifies and returns +self+
54
+ def combine!(other)
55
+ return self if other == self
56
+
57
+ @transaction_count += other.transaction_count
58
+ @call_count += other.call_count
59
+ @rows_returned += other.rows_returned
60
+ @call_time += other.call_time
61
+
62
+ @min_call_time = other.min_call_time if @min_call_time.zero? or other.min_call_time < @min_call_time
63
+ @max_call_time = other.max_call_time if other.max_call_time > @max_call_time
64
+
65
+ @min_rows_returned = other.min_rows_returned if @min_rows_returned.zero? or other.min_rows_returned < @min_rows_returned
66
+ @max_rows_returned = other.max_rows_returned if other.max_rows_returned > @max_rows_returned
67
+
68
+ @histogram.combine!(other.histogram)
69
+ self
70
+ end
71
+
72
+ def as_json
73
+ json_attributes = [
74
+ :model_name,
75
+ :operation,
76
+ :scope,
77
+
78
+ :transaction_count,
79
+ :call_count,
80
+
81
+ :histogram,
82
+ :call_time,
83
+ :max_call_time,
84
+ :min_call_time,
85
+
86
+ :max_rows_returned,
87
+ :min_rows_returned,
88
+ :rows_returned,
89
+ ]
90
+
91
+ ScoutApm::AttributeArranger.call(self, json_attributes)
92
+ end
93
+
94
+ # Called by the Set on each DbQueryMetricStats object that it holds, only
95
+ # once during the recording of a transaction.
96
+ #
97
+ # Don't call elsewhere, and don't set to 1 in the initializer.
98
+ def increment_transaction_count!
99
+ @transaction_count += 1
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,37 @@
1
+ module ScoutApm
2
+ class Debug
3
+ # see self.instance
4
+ @@instance = nil
5
+
6
+ def self.instance
7
+ @@instance ||= new
8
+ end
9
+
10
+ def register_periodic_hook(&hook)
11
+ @periodic_hooks << hook
12
+ end
13
+
14
+ def call_periodic_hooks
15
+ @periodic_hooks.each do |hook|
16
+ begin
17
+ hook.call
18
+ rescue => e
19
+ logger.info("Periodic debug hook failed to run: #{e}\n\t#{e.backtrace.join("\n\t")}")
20
+ end
21
+ end
22
+ rescue
23
+ # Something went super wrong for the inner rescue to not catch this. Just
24
+ # swallow the error. The debug tool should never crash the app.
25
+ end
26
+
27
+ private
28
+
29
+ def initialize
30
+ @periodic_hooks = []
31
+ end
32
+
33
+ def logger
34
+ ScoutApm::Agent.instance.logger
35
+ end
36
+ end
37
+ end
@@ -24,8 +24,9 @@ module ScoutApm
24
24
  ]
25
25
 
26
26
  BACKGROUND_JOB_INTEGRATIONS = [
27
+ ScoutApm::BackgroundJobIntegrations::Resque.new,
27
28
  ScoutApm::BackgroundJobIntegrations::Sidekiq.new,
28
- # ScoutApm::BackgroundJobIntegrations::DelayedJob.new
29
+ ScoutApm::BackgroundJobIntegrations::DelayedJob.new,
29
30
  ]
30
31
 
31
32
  FRAMEWORK_INTEGRATIONS = [
@@ -41,13 +42,8 @@ module ScoutApm
41
42
  ScoutApm::PlatformIntegrations::Server.new,
42
43
  ]
43
44
 
44
- DEPLOY_INTEGRATIONS = [
45
- ScoutApm::DeployIntegrations::Capistrano3.new(STDOUT_LOGGER),
46
- # ScoutApm::DeployIntegrations::Capistrano2.new(STDOUT_LOGGER),
47
- ]
48
-
49
45
  def env
50
- @env ||= deploy_integration? ? deploy_integration.env : framework_integration.env
46
+ @env ||= framework_integration.env
51
47
  end
52
48
 
53
49
  def framework
@@ -63,7 +59,9 @@ module ScoutApm
63
59
  end
64
60
 
65
61
  def application_name
66
- Agent.instance.config.value("name") || framework_integration.application_name
62
+ Agent.instance.config.value("name") ||
63
+ framework_integration.application_name ||
64
+ "App"
67
65
  end
68
66
 
69
67
  def database_engine
@@ -87,8 +85,16 @@ module ScoutApm
87
85
  end
88
86
  end
89
87
 
88
+ def scm_subdirectory
89
+ @scm_subdirectory ||= if Agent.instance.config.value('scm_subdirectory').empty?
90
+ ''
91
+ else
92
+ Agent.instance.config.value('scm_subdirectory').sub(/^\//, '') # Trim any leading slash
93
+ end
94
+ end
95
+
90
96
  def root
91
- @root ||= deploy_integration? ? deploy_integration.root : framework_root
97
+ @root ||= framework_root
92
98
  end
93
99
 
94
100
  def framework_root
@@ -110,6 +116,10 @@ module ScoutApm
110
116
  @hostname ||= Agent.instance.config.value("hostname") || platform_integration.hostname
111
117
  end
112
118
 
119
+ def git_revision
120
+ @git_revision ||= ScoutApm::GitRevision.new
121
+ end
122
+
113
123
  # Returns the whole integration object
114
124
  # This needs to be improved. Frequently, multiple app servers gem are present and which
115
125
  # ever is checked first becomes the designated app server.
@@ -143,12 +153,9 @@ module ScoutApm
143
153
  background_job_integration && background_job_integration.name
144
154
  end
145
155
 
146
- def deploy_integration
147
- @deploy_integration ||= DEPLOY_INTEGRATIONS.detect{ |integration| integration.present? }
148
- end
149
-
150
- def deploy_integration?
151
- !@deploy_integration.nil?
156
+ # If both stdin & stdout are interactive and the Rails::Console constant is defined
157
+ def interactive?
158
+ defined?(::Rails::Console) && $stdout.isatty && $stdin.isatty
152
159
  end
153
160
 
154
161
  ### ruby checks
@@ -0,0 +1,51 @@
1
+ module ScoutApm
2
+ class GitRevision
3
+
4
+ attr_accessor :sha
5
+
6
+ def initialize
7
+ @sha = detect
8
+ ScoutApm::Agent.instance.logger.debug "Detected Git Revision [#{@sha}]"
9
+ end
10
+
11
+ private
12
+
13
+ def detect
14
+ detect_from_env_var ||
15
+ detect_from_heroku ||
16
+ detect_from_capistrano ||
17
+ detect_from_git
18
+ end
19
+
20
+ def detect_from_heroku
21
+ ENV['HEROKU_SLUG_COMMIT']
22
+ end
23
+
24
+ def detect_from_env_var
25
+ ENV['SCOUT_REVISION_SHA']
26
+ end
27
+
28
+ def detect_from_capistrano
29
+ version = File.read(File.join(app_root, 'REVISION')).strip
30
+ # Capistrano 3.0 - 3.1.x
31
+ version || File.open(File.join(app_root, '..', 'revisions.log')).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1')
32
+ rescue
33
+ ScoutApm::Agent.instance.logger.debug "Unable to detect Git Revision from Capistrano: #{$!.message}"
34
+ nil
35
+ end
36
+
37
+ def detect_from_git
38
+ if File.directory?(".git")
39
+ `git rev-parse --short HEAD`.strip
40
+ end
41
+ rescue
42
+ ScoutApm::Agent.instance.logger.debug "Unable to detect Git Revision from Git: #{$!.message}"
43
+ nil
44
+ end
45
+
46
+ def app_root
47
+ ScoutApm::Environment.instance.root
48
+ end
49
+
50
+ end
51
+ end
@@ -90,7 +90,11 @@ module ScoutApm
90
90
  def combine!(other)
91
91
  mutex.synchronize do
92
92
  other.mutex.synchronize do
93
- @bins = (other.bins + @bins).sort_by {|b| b.value }
93
+ @bins = (other.bins + @bins).
94
+ group_by {|b| b.value }.
95
+ map {|val, bs| [val, bs.inject(0) {|sum, b| sum + b.count }] }.
96
+ map {|val, sum| HistogramBin.new(val,sum) }.
97
+ sort_by { |b| b.value }
94
98
  @total += other.total
95
99
  trim
96
100
  self
@@ -100,7 +104,12 @@ module ScoutApm
100
104
 
101
105
  def as_json
102
106
  mutex.synchronize do
103
- bins.map{|b| [b.value, b.count]}
107
+ bins.map{ |b|
108
+ [
109
+ ScoutApm::Utils::Numbers.round(b.value, 4),
110
+ b.count
111
+ ]
112
+ }
104
113
  end
105
114
  end
106
115
 
@@ -3,7 +3,7 @@
3
3
  (function(){var open=window.XMLHttpRequest.prototype.open;var send=window.XMLHttpRequest.prototype.send;function openReplacement(method,url,async,user,password){this._url=url;return open.apply(this,arguments);}
4
4
  function sendReplacement(data){if(this.onload){this._onload=this.onload;}
5
5
  this.onload=onLoadReplacement;return send.apply(this,arguments);}
6
- function onLoadReplacement(){if(this._url.startsWith(window.location.protocol+"//"+window.location.host)||!this._url.startsWith("http")){try{traceText=this.getResponseHeader("X-scoutapminstant");if(traceText){setTimeout(function(){window.scoutInstant("addTrace",traceText)},0);}}catch(e){console.debug("Problem getting X-scoutapminstant header");}}
6
+ function onLoadReplacement(){if(this._url.startsWith(window.location.protocol+"//"+window.location.host)||!this._url.startsWith("http")){try{var traceText=this.getResponseHeader("X-scoutapminstant");if(traceText){setTimeout(function(){window.scoutInstant("addTrace",traceText)},0);}}catch(e){console.debug("Problem getting X-scoutapminstant header");}}
7
7
  if(this._onload){return this._onload.apply(this,arguments);}}
8
8
  window.XMLHttpRequest.prototype.open=openReplacement;window.XMLHttpRequest.prototype.send=sendReplacement;})();
9
- </script>
9
+ </script>
@@ -5,6 +5,11 @@ module ScoutApm
5
5
  class Page
6
6
  def initialize(html)
7
7
  @html = html
8
+
9
+ if html.is_a?(Array)
10
+ @html = html.inject("") { |memo, str| memo + str }
11
+ end
12
+
8
13
  @to_add_to_head = []
9
14
  @to_add_to_body = []
10
15
  end
@@ -45,63 +50,200 @@ module ScoutApm
45
50
  end
46
51
 
47
52
  def call(env)
48
- status, headers, response = @app.call(env)
49
- path, content_type = env['PATH_INFO'], headers['Content-Type']
50
- if ScoutApm::Agent.instance.config.value('dev_trace')
51
- if response.respond_to?(:body)
52
- req = ScoutApm::RequestManager.lookup
53
- slow_converter = LayerConverters::SlowRequestConverter.new(req)
54
- trace = slow_converter.call
55
- if trace
56
- metadata = {
57
- :app_root => ScoutApm::Environment.instance.root.to_s,
58
- :unique_id => env['action_dispatch.request_id'], # note, this is a different unique_id than what "normal" payloads use
59
- :agent_version => ScoutApm::VERSION,
60
- :platform => "ruby",
61
- }
62
- hash = ScoutApm::Serializers::PayloadSerializerToJson.rearrange_slow_transaction(trace)
63
- hash.merge!(:metadata => metadata)
64
- payload = ScoutApm::Serializers::PayloadSerializerToJson.jsonify_hash(hash)
65
-
66
- if env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' || content_type.include?("application/json")
67
- ScoutApm::Agent.instance.logger.debug("DevTrace: in middleware, dev_trace is active, and response has a body. This is either AJAX or JSON. Path=#{path}; ContentType=#{content_type}")
68
- # Add the payload as a header if it's an AJAX call or JSON
69
- headers['X-scoutapminstant'] = payload
70
- [status, headers, response]
71
- else
72
- # otherwise, attempt to add it inline in the page, along with the appropriate JS & CSS. Note, if page doesn't have a head or body,
73
- #duration = (req.root_layer.total_call_time*1000).to_i
74
- apm_host=ScoutApm::Agent.instance.config.value("direct_host")
75
- page = ScoutApm::Instant::Page.new(response.body)
76
- page.add_to_head(ScoutApm::Instant::Util.read_asset("xmlhttp_instrumentation.html")) # This monkey-patches XMLHttpRequest. It could possibly be part of the main scout_instant.js too. Putting it here so it runs as soon as possible.
77
- page.add_to_head("<link href='#{apm_host}/instant/scout_instant.css?cachebust=#{Time.now.to_i}' media='all' rel='stylesheet' />")
78
- page.add_to_body("<script src='#{apm_host}/instant/scout_instant.js?cachebust=#{Time.now.to_i}'></script>")
79
- page.add_to_body("<script>var scoutInstantPageTrace=#{payload};window.scoutInstant=window.scoutInstant('#{apm_host}', scoutInstantPageTrace)</script>")
80
-
81
- if response.is_a?(ActionDispatch::Response)
82
- ScoutApm::Agent.instance.logger.debug("DevTrace: in middleware, dev_trace is active, and response has a body. This appears to be an HTML page and an ActionDispatch::Response. Path=#{path}; ContentType=#{content_type}")
83
- # preserve the ActionDispatch::Response when applicable
84
- response.body=[page.res]
85
- [status, headers, response]
86
- else
87
- ScoutApm::Agent.instance.logger.debug("DevTrace: in middleware, dev_trace is active, and response has a body. This appears to be an HTML page but not an ActionDispatch::Response. Path=#{path}; ContentType=#{content_type}")
88
- # otherwise, just return an array
89
- [status, headers, [page.res]]
90
- end
91
- end
92
- else
93
- ScoutApm::Agent.instance.logger.debug("DevTrace: in middleware, dev_trace is active, and response has a body, but no trace was found. Path=#{path}; ContentType=#{content_type}")
94
- [status, headers, response]
95
- end
96
- else
97
- # don't log anything here - this is the path for all assets served in development, and the log would get noisy
98
- [status, headers, response]
99
- end
53
+ rack_response = @app.call(env)
54
+ begin
55
+ DevTraceResponseManipulator.new(env, rack_response).call
56
+ rescue Exception => e
57
+ # If anything went wrong at all, just bail out and return the unmodified response.
58
+ ScoutApm::Agent.instance.logger.debug("DevTrace: Raised an exception: #{e.message}, #{e.backtrace}")
59
+ rack_response
60
+ end
61
+ end
62
+ end
63
+
64
+ class DevTraceResponseManipulator
65
+ attr_reader :rack_response
66
+ attr_reader :rack_status, :rack_headers, :rack_body
67
+ attr_reader :env
68
+
69
+ def initialize(env, rack_response)
70
+ @env = env
71
+ @rack_response = rack_response
72
+
73
+ @rack_status = rack_response[0]
74
+ @rack_headers = rack_response[1]
75
+ @rack_body = rack_response[2]
76
+ end
77
+
78
+ def call
79
+ return rack_response unless preconditions_met?
80
+
81
+ if ajax_request?
82
+ ScoutApm::Agent.instance.logger.debug("DevTrace: in middleware, dev_trace is active, and response has a body. This is either AJAX or JSON. Path=#{path}; ContentType=#{content_type}")
83
+ adjust_ajax_header
84
+ else
85
+ adjust_html_response
86
+ end
87
+
88
+ rebuild_rack_response
89
+ end
90
+
91
+ ###########################
92
+ # Precondition checking #
93
+ ###########################
94
+
95
+ def preconditions_met?
96
+ if dev_trace_disabled?
97
+ logger.debug("DevTrace: isn't activated via config. Try: SCOUT_DEV_TRACE=true rails server")
98
+ return false
99
+ end
100
+
101
+ # Don't attempt to instrument assets.
102
+ # Don't log this case, since it would be very noisy
103
+ logger.debug("DevTrace: dev asset ignored") and return false if development_asset?
104
+
105
+ # If we didn't have a tracked_request object, or we explicitly ignored
106
+ # this request, don't do any work.
107
+ logger.debug("DevTrace: no tracked request") and return false if tracked_request.nil? || tracked_request.ignoring_request?
108
+
109
+ # If we didn't get a trace, we can't show a trace...
110
+ if trace.nil?
111
+ logger.debug("DevTrace: in middleware, dev_trace is active, and response has a body, but no trace was found. Path=#{path}; ContentType=#{content_type}")
112
+ return false
113
+ end
114
+
115
+ true
116
+ end
117
+
118
+ def dev_trace_disabled?
119
+ ! ScoutApm::Agent.instance.config.value('dev_trace')
120
+ end
121
+
122
+ ########################
123
+ # Response Injection #
124
+ ########################
125
+
126
+ def rebuild_rack_response
127
+ [rack_status, rack_headers, rack_body]
128
+ end
129
+
130
+ def adjust_ajax_header
131
+ rack_headers['X-scoutapminstant'] = payload
132
+ end
133
+
134
+ def adjust_html_response
135
+ case true
136
+ when older_rails_response? then adjust_older_rails_response
137
+ when newer_rails_response? then adjust_newer_rails_response
138
+ when rack_proxy_response? then adjust_rack_proxy_response
100
139
  else
101
- ScoutApm::Agent.instance.logger.debug("DevTrace: isn't activated via config. Try: SCOUT_DEV_TRACE=true rails server")
102
- [status, headers, response]
140
+ # No action taken, we only adjust if we know exactly what we have.
141
+ end
142
+ end
143
+
144
+ def older_rails_response?
145
+ if defined?(ActionDispatch::Response)
146
+ return true if rack_body.is_a?(ActionDispatch::Response)
147
+ end
148
+ end
149
+
150
+ def newer_rails_response?
151
+ if defined?(ActionDispatch::Response::RackBody)
152
+ return true if rack_body.is_a?(ActionDispatch::Response::RackBody)
103
153
  end
104
154
  end
155
+
156
+ def rack_proxy_response?
157
+ rack_body.is_a?(Rack::BodyProxy)
158
+ end
159
+
160
+ def adjust_older_rails_response
161
+ logger.debug("DevTrace: in middleware, dev_trace is active, and response has a (older) body. This appears to be an HTML page and an ActionDispatch::Response. Path=#{path}; ContentType=#{content_type}")
162
+ rack_body.body = [ html_manipulator.res ]
163
+ end
164
+
165
+ # Preserve the ActionDispatch::Response object we're working with
166
+ def adjust_newer_rails_response
167
+ logger.debug("DevTrace: in middleware, dev_trace is active, and response has a (newer) body. This appears to be an HTML page and an ActionDispatch::Response. Path=#{path}; ContentType=#{content_type}")
168
+ @rack_body = [ html_manipulator.res ]
169
+ end
170
+
171
+ def adjust_rack_proxy_response
172
+ logger.debug("DevTrace: in middleware, dev_trace is active, and response has a body. This appears to be an HTML page and an Rack::BodyProxy. Path=#{path}; ContentType=#{content_type}")
173
+ @rack_body = [ html_manipulator.res ]
174
+ @rack_headers.delete("Content-Length")
175
+ end
176
+
177
+ def html_manipulator
178
+ @html_manipulator ||=
179
+ begin
180
+ page = ScoutApm::Instant::Page.new(rack_body.body)
181
+
182
+ # This monkey-patches XMLHttpRequest. It could possibly be part of the main scout_instant.js too. Putting it here so it runs as soon as possible.
183
+ page.add_to_head(ScoutApm::Instant::Util.read_asset("xmlhttp_instrumentation.html"))
184
+
185
+ # Add a link to CSS, then JS
186
+ page.add_to_head("<link href='#{apm_host}/instant/scout_instant.css?cachebust=#{Time.now.to_i}' media='all' rel='stylesheet' />")
187
+ page.add_to_body("<script src='#{apm_host}/instant/scout_instant.js?cachebust=#{Time.now.to_i}'></script>")
188
+ page.add_to_body("<script>var scoutInstantPageTrace=#{payload};window.scoutInstant=window.scoutInstant('#{apm_host}', scoutInstantPageTrace)</script>")
189
+
190
+ page
191
+ end
192
+ end
193
+
194
+ def ajax_request?
195
+ env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' || content_type.include?("application/json")
196
+ end
197
+
198
+ def development_asset?
199
+ !rack_body.respond_to?(:body)
200
+ end
201
+
202
+ def path
203
+ env['PATH_INFO']
204
+ end
205
+
206
+ def content_type
207
+ rack_headers['Content-Type']
208
+ end
209
+
210
+ ##############################
211
+ # APM Helpers & Shorthands #
212
+ ##############################
213
+
214
+ def logger
215
+ ScoutApm::Agent.instance.logger
216
+ end
217
+
218
+ def tracked_request
219
+ @tracked_request ||= ScoutApm::RequestManager.lookup
220
+ end
221
+
222
+ def apm_host
223
+ ScoutApm::Agent.instance.config.value("direct_host")
224
+ end
225
+
226
+ def trace
227
+ @trace ||= LayerConverters::SlowRequestConverter.new(tracked_request).call
228
+ end
229
+
230
+ def payload
231
+ @payload ||=
232
+ begin
233
+ metadata = {
234
+ :app_root => ScoutApm::Environment.instance.root.to_s,
235
+ :unique_id => env['action_dispatch.request_id'], # note, this is a different unique_id than what "normal" payloads use
236
+ :agent_version => ScoutApm::VERSION,
237
+ :platform => "ruby",
238
+ }
239
+
240
+ hash = ScoutApm::Serializers::PayloadSerializerToJson.
241
+ rearrange_slow_transaction(trace).
242
+ merge!(:metadata => metadata)
243
+ ScoutApm::Serializers::PayloadSerializerToJson.jsonify_hash(hash)
244
+ end
245
+ end
246
+
105
247
  end
106
248
  end
107
249
  end