scout_apm 2.5.3 → 2.6.0

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
  SHA256:
3
- metadata.gz: ef6a530f826be85aee200f178152090c96dc8f2bab731c0f5220fa95a4662f56
4
- data.tar.gz: e8ab6688fec666ff02c76a996dff3daf84fa1ca84be19b2666ba13b256f9fa92
3
+ metadata.gz: a0d5d02cc644b23e02868a9705fba1a15f0250bba5bff1c92eb97efaf04f1d87
4
+ data.tar.gz: e0f5acef9895941f7d2f327596d1f9eef9f6a3ef374ddced1ee658b2882bc525
5
5
  SHA512:
6
- metadata.gz: 4b8d53395a108407d445d1c386c1234ddbfa90e7105249ce357d288b92c5c9727a9c110a6e3510a111b6a4a9788f5e5f3a3eaedc532683cecbc09a8261c2f572
7
- data.tar.gz: 3a35a76ed54a96df86948ad486d5a86a9dffed67271c5efae229b80a8106c00eb3830be469875cd301f7a262fba34672a8db52a25d4e82cba5105ce80af26dde
6
+ metadata.gz: 976b31dc166e501368825e07b3377138be135790a23a92fc52681f81d780034641d86496ac0d03dda0639127673d6b1908f5191f966a9193488094a24a7720a7
7
+ data.tar.gz: 835d1e4f4db51c71a41521f5892e91d1d67c7cd96effc91f684bacbe6fa9e9303e9abdb9b697c75d79d656bab45d9b241b3200ac8203236fb75a07c101f3c5fb
data/.rubocop.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  # Disable all cops by default
2
2
  AllCops:
3
3
  DisabledByDefault: true
4
+ Exclude:
5
+ - 'test/unit/auto_instrument/*'
6
+ - vendor/bundle/**/*
4
7
 
5
8
  # 80 is stifling, especially with a few levels of nesting before we even start.
6
9
  # So bump it to 100 to keep really long lines from creeping in.
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,7 @@
1
+ # 2.6.0
2
+
3
+ * Autoinstruments (#247). Disabled by default. Set `auto_instruments: true` to enable.
4
+
1
5
  # 2.5.3
2
6
 
3
7
  * Add Que support (#265)
@@ -27,7 +31,7 @@
27
31
 
28
32
  # 2.4.22
29
33
 
30
- * Support Rails 6.0 View Instruments (#251)
34
+ * Support Rails 6.0 View Instruments (#251)
31
35
  * Update documentation URLs (#236)
32
36
 
33
37
  # 2.4.21
@@ -675,7 +679,7 @@ Big set of features getting merged in for this release.
675
679
 
676
680
  # 0.1.3
677
681
 
678
- * Adds capacity calculation via "Instance/Capacity" metric.
682
+ * Adds capacity calculation via "Instance/Capacity" metric.
679
683
  * Tweaks tracing to still count a transaction if it results in a 500 error and includes it in accumulated time.
680
684
  * Adds per-transaction error tracking (ex: Errors/Controller/widgets/index)
681
685
 
data/lib/scout_apm.rb CHANGED
@@ -196,6 +196,13 @@ if defined?(Rails) && defined?(Rails::VERSION) && defined?(Rails::VERSION::MAJOR
196
196
  # Attempt to start right away, this will work best for preloading apps, Unicorn & Puma & similar
197
197
  ScoutApm::Agent.instance.install
198
198
 
199
+ if ScoutApm::Agent.instance.context.config.value("auto_instruments")
200
+ ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is enabled.")
201
+ require 'scout_apm/auto_instrument'
202
+ else
203
+ ScoutApm::Agent.instance.context.logger.debug("AutoInstruments is disabled.")
204
+ end
205
+
199
206
  # Install the middleware every time in development mode.
200
207
  # The middleware is a noop if dev_trace is not enabled in config
201
208
  if Rails.env.development?
@@ -0,0 +1,5 @@
1
+ if RUBY_VERSION >= "2.3"
2
+ require 'scout_apm/auto_instrument/instruction_sequence'
3
+ else
4
+ warn "ScoutApm::AutoInstrument requires Ruby >= v2.3. Skipping."
5
+ end
@@ -0,0 +1,29 @@
1
+
2
+ require 'scout_apm/auto_instrument/rails'
3
+
4
+ module ScoutApm
5
+ module AutoInstrument
6
+ module InstructionSequence
7
+ def load_iseq(path)
8
+ if Rails.controller_path?(path)
9
+ begin
10
+ new_code = Rails.rewrite(path)
11
+ return self.compile(new_code, File.basename(path), path)
12
+ rescue
13
+ warn "Failed to apply auto-instrumentation to #{path}: #{$!}"
14
+ end
15
+ end
16
+
17
+ return self.compile_file(path)
18
+ end
19
+ end
20
+
21
+ # This should work (https://bugs.ruby-lang.org/issues/15572), but it doesn't.
22
+ # RubyVM::InstructionSequence.extend(InstructionSequence)
23
+
24
+ # So we do this instead:
25
+ class << ::RubyVM::InstructionSequence
26
+ prepend InstructionSequence
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+
2
+ module ScoutApm
3
+ def self.AutoInstrument(name, backtrace = nil)
4
+ request = ScoutApm::RequestManager.lookup
5
+
6
+ begin
7
+ layer = ScoutApm::Layer.new('AutoInstrument', name)
8
+ layer.backtrace = backtrace
9
+
10
+ request.start_layer(layer)
11
+ started_layer = true
12
+
13
+ result = yield
14
+ ensure
15
+ request.stop_layer if started_layer
16
+ end
17
+
18
+ return result
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+
2
+ require 'parser/current'
3
+ raise LoadError, "Parser::TreeRewriter was not defined" unless defined?(Parser::TreeRewriter)
4
+
5
+ module ScoutApm
6
+ module AutoInstrument
7
+ class Cache
8
+ def initialize
9
+ @local_assignments = {}
10
+ end
11
+
12
+ def local_assignments?(node)
13
+ unless @local_assignments.key?(node)
14
+ if node.type == :lvasgn
15
+ @local_assignments[node] = true
16
+ elsif node.children.find{|child| child.is_a?(Parser::AST::Node) && self.local_assignments?(child)}
17
+ @local_assignments[node] = true
18
+ else
19
+ @local_assignments[node] = false
20
+ end
21
+ end
22
+
23
+ return @local_assignments[node]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,158 @@
1
+
2
+ require 'scout_apm/auto_instrument/layer'
3
+ require 'scout_apm/auto_instrument/parser'
4
+
5
+ module ScoutApm
6
+ module AutoInstrument
7
+ module Rails
8
+ # A general pattern to match Rails controller files:
9
+ CONTROLLER_FILE = /\/app\/controllers\/*\/.*_controller.rb$/.freeze
10
+
11
+ # Some gems (Devise) provide controllers that match CONTROLLER_FILE pattern.
12
+ # Try a simple match to see if it's a Gemfile
13
+ GEM_FILE = /\/gems?\//.freeze
14
+
15
+ # Whether the given path is likely to be a Rails controller and not provided by a Gem.
16
+ def self.controller_path? path
17
+ CONTROLLER_FILE.match(path) && !GEM_FILE.match(path)
18
+ end
19
+
20
+ def self.rewrite(path, code = nil)
21
+ code ||= File.read(path)
22
+
23
+ ast = ::Parser::CurrentRuby.parse(code)
24
+
25
+ # pp ast
26
+
27
+ buffer = ::Parser::Source::Buffer.new(path)
28
+ buffer.source = code
29
+
30
+ rewriter = Rewriter.new
31
+
32
+ # Rewrite the AST, returns a String with the new form.
33
+ rewriter.rewrite(buffer, ast)
34
+ end
35
+
36
+ class Rewriter < ::Parser::TreeRewriter
37
+ def initialize
38
+ super
39
+
40
+ # Keeps track of the parent - child relationship between nodes:
41
+ @nesting = []
42
+
43
+ # The stack of method nodes (type :def):
44
+ @method = []
45
+
46
+ # The stack of class nodes:
47
+ @scope = []
48
+
49
+ @cache = Cache.new
50
+ end
51
+
52
+ def instrument(source, file_name, line)
53
+ # Don't log huge chunks of code... just the first line:
54
+ if lines = source.lines and lines.count > 1
55
+ source = lines.first.chomp + "..."
56
+ end
57
+
58
+ method_name = @method.last.children[0]
59
+ class_name = @scope.last.children[1]
60
+ bt = ["#{file_name}:#{line}:in `#{method_name}'"]
61
+
62
+ return [
63
+ "::ScoutApm::AutoInstrument("+ source.dump + ",#{bt}" + "){",
64
+ "}"
65
+ ]
66
+ end
67
+
68
+ # Look up 1 or more nodes to check if the parent exists and matches the given type.
69
+ # @param type [Symbol] the symbol type to match.
70
+ # @param up [Integer] how far up to look.
71
+ def parent_type?(type, up = 1)
72
+ parent = @nesting[@nesting.size - up - 1] and parent.type == type
73
+ end
74
+
75
+ def on_block(node)
76
+ # If we are not in a method, don't do any instrumentation:
77
+ return if @method.empty?
78
+
79
+ line = node.location.line || 'line?'
80
+ column = node.location.column || 'column?' # not used
81
+ method_name = node.children[0].children[1] || '*unknown*' # not used
82
+ file_name = @source_rewriter.source_buffer.name
83
+
84
+ wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
85
+ end
86
+
87
+ def on_mlhs(node)
88
+ # Ignore / don't instrument multiple assignment (LHS).
89
+ return
90
+ end
91
+
92
+ def on_or_asgn(node)
93
+ process(node.children[1])
94
+ end
95
+
96
+ def on_and_asgn(node)
97
+ process(node.children[1])
98
+ end
99
+
100
+ # Handle the method call AST node. If this method doesn't call `super`, no futher rewriting is applied to children.
101
+ def on_send(node)
102
+ # We aren't interested in top level function calls:
103
+ return if @method.empty?
104
+
105
+ if @cache.local_assignments?(node)
106
+ return super
107
+ end
108
+
109
+ # This ignores both initial block method invocation `*x*{}`, and subsequent nested invocations `x{*y*}`:
110
+ return if parent_type?(:block)
111
+
112
+ # Extract useful metadata for instrumentation:
113
+ line = node.location.line || 'line?'
114
+ column = node.location.column || 'column?' # not used
115
+ method_name = node.children[1] || '*unknown*' # not used
116
+ file_name = @source_rewriter.source_buffer.name
117
+
118
+ # Wrap the expression with instrumentation:
119
+ wrap(node.location.expression, *instrument(node.location.expression.source, file_name, line))
120
+ end
121
+
122
+ # def on_class(node)
123
+ # class_name = node.children[1]
124
+ #
125
+ # Kernel.const_get(class_name).ancestors.include? ActionController::Controller
126
+ #
127
+ # if class_name =~ /.../
128
+ # super # continue processing
129
+ # end
130
+ # end
131
+
132
+ # Invoked for every AST node as it is processed top to bottom.
133
+ def process(node)
134
+ # We are nesting inside this node:
135
+ @nesting.push(node)
136
+
137
+ if node and node.type == :def
138
+ # If the node is a method, push it on the method stack as well:
139
+ @method.push(node)
140
+ super
141
+ @method.pop
142
+ elsif node and node.type == :class
143
+ @scope.push(node.children[0])
144
+ super
145
+ @scope.pop
146
+ else
147
+ super
148
+ end
149
+
150
+ @nesting.pop
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ # Force any lazy loading to occur here, before we patch iseq_load. Otherwise you might end up in an infinite loop when rewriting code.
158
+ ScoutApm::AutoInstrument::Rails.rewrite('(preload)', '')
@@ -33,6 +33,7 @@ require 'scout_apm/environment'
33
33
  # remote_agent_port - What port to bind the remote webserver to
34
34
  # start_resque_server_instrument - Used in special situations with certain Resque installs
35
35
  # timeline_traces - true/false to enable sending of of the timeline trace format.
36
+ # auto_instruments - true/false whether to install autoinstruments. Only installed if on a supported Ruby version.
36
37
  #
37
38
  # Any of these config settings can be set with an environment variable prefixed
38
39
  # by SCOUT_ and uppercasing the key: SCOUT_LOG_LEVEL for instance.
@@ -75,7 +76,8 @@ module ScoutApm
75
76
  'start_resque_server_instrument',
76
77
  'uri_reporting',
77
78
  'instrument_http_url_length',
78
- 'timeline_traces'
79
+ 'timeline_traces',
80
+ 'auto_instruments'
79
81
  ]
80
82
 
81
83
  ################################################################################
@@ -168,7 +170,8 @@ module ScoutApm
168
170
  'database_metric_report_limit' => IntegerCoercion.new,
169
171
  'instrument_http_url_length' => IntegerCoercion.new,
170
172
  'start_resque_server_instrument' => BooleanCoercion.new,
171
- 'timeline_traces' => BooleanCoercion.new
173
+ 'timeline_traces' => BooleanCoercion.new,
174
+ 'auto_instruments' => BooleanCoercion.new
172
175
  }
173
176
 
174
177
 
@@ -276,7 +279,8 @@ module ScoutApm
276
279
  'instrument_http_url_length' => 300,
277
280
  'start_resque_server_instrument' => true, # still only starts if Resque is detected
278
281
  'collect_remote_ip' => true,
279
- 'timeline_traces' => true
282
+ 'timeline_traces' => true,
283
+ 'auto_instruments' => false
280
284
  }.freeze
281
285
 
282
286
  def value(key)
@@ -36,7 +36,7 @@ module ScoutApm
36
36
 
37
37
  # If this layer took longer than a fixed amount of time, store the
38
38
  # backtrace of where it occurred.
39
- attr_reader :backtrace
39
+ attr_accessor :backtrace
40
40
 
41
41
  # As we go through a part of a request, instrumentation can store additional data
42
42
  # Known Keys:
@@ -52,6 +52,10 @@ module ScoutApm
52
52
  # see that on Sidekiq.
53
53
  REQUEST_TYPES = ["Controller", "Job"]
54
54
 
55
+ # Layers of type 'AutoInstrument' are not recorded if their total_call_time doesn't exceed this threshold.
56
+ # AutoInstrument layers are frequently of short duration. This throws out this deadweight that is unlikely to be optimized.
57
+ AUTO_INSTRUMENT_TIMING_THRESHOLD = 5/1_000.0 # units = seconds
58
+
55
59
  def initialize(agent_context, store)
56
60
  @agent_context = agent_context
57
61
  @store = store #this is passed in so we can use a real store (normal operation) or fake store (instant mode only)
@@ -110,6 +114,9 @@ module ScoutApm
110
114
  layer.record_stop_time!
111
115
  layer.record_allocations!
112
116
 
117
+ # Must follow layer.record_stop_time! as the total_call_time is used to determine if the layer is significant.
118
+ return if layer_insignificant?(layer)
119
+
113
120
  @layers[-1].add_child(layer) if @layers.any?
114
121
 
115
122
  # This must be called before checking if a backtrace should be collected as the call count influences our capture logic.
@@ -150,6 +157,10 @@ module ScoutApm
150
157
  def capture_backtrace?(layer)
151
158
  return if ignoring_request?
152
159
 
160
+ # A backtrace has already been recorded. This happens with autoinstruments as
161
+ # the partial backtrace is set when creating the layer.
162
+ return false if layer.backtrace
163
+
153
164
  # Never capture backtraces for this kind of layer. The backtrace will
154
165
  # always be 100% framework code.
155
166
  return false if BACKTRACE_BLACKLIST.include?(layer.type)
@@ -169,6 +180,16 @@ module ScoutApm
169
180
  false
170
181
  end
171
182
 
183
+ def layer_insignificant?(layer)
184
+ if layer.type == 'AutoInstrument'
185
+ if layer.total_call_time < AUTO_INSTRUMENT_TIMING_THRESHOLD
186
+ context.logger.debug("IGNORE LAYER name=#{layer.name} total_call_time=#{layer.total_call_time}")
187
+ return true
188
+ end
189
+ end
190
+ return false
191
+ end
192
+
172
193
  # Maintains a lookup Hash of call counts by layer name. Used to determine if we should capture a backtrace.
173
194
  def update_call_counts!(layer)
174
195
  @call_set[layer.name].update!(layer.desc)
@@ -11,6 +11,9 @@ module ScoutApm
11
11
 
12
12
  attr_reader :call_stack
13
13
 
14
+ # call_stack - an +Array+ of calls, typically generated via the +caller+ method.
15
+ # Example single line:
16
+ # "/Users/dlite/.rvm/rubies/ruby-2.4.5/lib/ruby/2.4.0/irb/workspace.rb:87:in `eval'"
14
17
  def initialize(call_stack, root=ScoutApm::Agent.instance.context.environment.root)
15
18
  @call_stack = call_stack
16
19
  # We can't use a constant as it'd be too early to fetch environment info
@@ -1,3 +1,3 @@
1
1
  module ScoutApm
2
- VERSION = "2.5.3"
2
+ VERSION = "2.6.0"
3
3
  end
data/scout_apm.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |s|
28
28
  s.add_development_dependency "rake-compiler"
29
29
  s.add_development_dependency "addressable"
30
30
  s.add_development_dependency "activesupport"
31
+ s.add_runtime_dependency "parser"
31
32
 
32
33
  # These are general development dependencies which are used in instrumentation
33
34
  # tests. Specific versions are pulled in using specific gemfiles, e.g.
@@ -0,0 +1,26 @@
1
+
2
+ class Assignments
3
+ def nested_assignment
4
+ @email ||= if (email = ::ScoutApm::AutoInstrument("session[\"email\"]",["BACKTRACE"]){session["email"]}).present?
5
+ ::ScoutApm::AutoInstrument("User.where(email: email).first",["BACKTRACE"]){User.where(email: email).first}
6
+ else
7
+ nil
8
+ end
9
+ end
10
+
11
+ def paginate_collection(coll)
12
+ page = (::ScoutApm::AutoInstrument("params[:page].present?",["BACKTRACE"]){params[:page].to_i} : 1)
13
+ per_page = (::ScoutApm::AutoInstrument("params[:per_page].present?",["BACKTRACE"]){params[:per_page].to_i} : 20)
14
+ pagination, self.collection = ::ScoutApm::AutoInstrument("pagy(...",["BACKTRACE"]){pagy(
15
+ coll,
16
+ items: per_page,
17
+ page: page
18
+ )}
19
+ ::ScoutApm::AutoInstrument("headers[PAGINATION_TOTAL_HEADER] = pagination.count.to_s",["BACKTRACE"]){headers[PAGINATION_TOTAL_HEADER] = pagination.count.to_s}
20
+ ::ScoutApm::AutoInstrument("headers[PAGINATION_TOTAL_PAGES_HEADER] = pagination.pages.to_s",["BACKTRACE"]){headers[PAGINATION_TOTAL_PAGES_HEADER] = pagination.pages.to_s}
21
+ ::ScoutApm::AutoInstrument("headers[PAGINATION_PER_PAGE_HEADER] = per_page.to_s",["BACKTRACE"]){headers[PAGINATION_PER_PAGE_HEADER] = per_page.to_s}
22
+ ::ScoutApm::AutoInstrument("headers[PAGINATION_PAGE_HEADER] = pagination.page.to_s",["BACKTRACE"]){headers[PAGINATION_PAGE_HEADER] = pagination.page.to_s}
23
+ ::ScoutApm::AutoInstrument("headers[PAGINATION_NEXT_PAGE_HEADER] = pagination.next.to_s",["BACKTRACE"]){headers[PAGINATION_NEXT_PAGE_HEADER] = pagination.next.to_s}
24
+ ::ScoutApm::AutoInstrument("collection",["BACKTRACE"]){collection}
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+
2
+ class Assignments
3
+ def nested_assignment
4
+ @email ||= if (email = session["email"]).present?
5
+ User.where(email: email).first
6
+ else
7
+ nil
8
+ end
9
+ end
10
+
11
+ def paginate_collection(coll)
12
+ page = (params[:page].present? ? params[:page].to_i : 1)
13
+ per_page = (params[:per_page].present? ? params[:per_page].to_i : 20)
14
+ pagination, self.collection = pagy(
15
+ coll,
16
+ items: per_page,
17
+ page: page
18
+ )
19
+ headers[PAGINATION_TOTAL_HEADER] = pagination.count.to_s
20
+ headers[PAGINATION_TOTAL_PAGES_HEADER] = pagination.pages.to_s
21
+ headers[PAGINATION_PER_PAGE_HEADER] = per_page.to_s
22
+ headers[PAGINATION_PAGE_HEADER] = pagination.page.to_s
23
+ headers[PAGINATION_NEXT_PAGE_HEADER] = pagination.next.to_s
24
+ collection
25
+ end
26
+ end
@@ -0,0 +1,57 @@
1
+ s(:class,
2
+ s(:const, nil, :ClientsController),
3
+ s(:const, nil, :ApplicationController),
4
+ s(:begin,
5
+ s(:send, nil, :before_action,
6
+ s(:sym, :check_authorization)),
7
+ s(:def, :index,
8
+ s(:args),
9
+ s(:if,
10
+ s(:send,
11
+ s(:send,
12
+ s(:send, nil, :params), :[],
13
+ s(:sym, :status)), :==,
14
+ s(:str, "activated")),
15
+ s(:ivasgn, :@clients,
16
+ s(:send,
17
+ s(:const, nil, :Client), :activated)),
18
+ s(:ivasgn, :@clients,
19
+ s(:send,
20
+ s(:const, nil, :Client), :inactivated)))),
21
+ s(:def, :create,
22
+ s(:args),
23
+ s(:begin,
24
+ s(:ivasgn, :@client,
25
+ s(:send,
26
+ s(:const, nil, :Client), :new,
27
+ s(:send,
28
+ s(:send, nil, :params), :[],
29
+ s(:sym, :client)))),
30
+ s(:if,
31
+ s(:send,
32
+ s(:ivar, :@client), :save),
33
+ s(:send, nil, :redirect_to,
34
+ s(:ivar, :@client)),
35
+ s(:send, nil, :render,
36
+ s(:str, "new"))))),
37
+ s(:def, :edit,
38
+ s(:args),
39
+ s(:begin,
40
+ s(:ivasgn, :@client,
41
+ s(:send,
42
+ s(:const, nil, :Client), :new,
43
+ s(:send,
44
+ s(:send, nil, :params), :[],
45
+ s(:sym, :client)))),
46
+ s(:if,
47
+ s(:send,
48
+ s(:send, nil, :request), :post?),
49
+ s(:block,
50
+ s(:send,
51
+ s(:ivar, :@client), :transaction),
52
+ s(:args),
53
+ s(:send,
54
+ s(:ivar, :@client), :update_attributes,
55
+ s(:send,
56
+ s(:send, nil, :params), :[],
57
+ s(:sym, :client)))), nil)))))
@@ -0,0 +1,49 @@
1
+
2
+ class ClientsController < ApplicationController
3
+ before_action :check_authorization
4
+
5
+ def index
6
+ if ::ScoutApm::AutoInstrument("params[:status] == \"activated\"",["BACKTRACE"]){params[:status] == "activated"}
7
+ @clients = ::ScoutApm::AutoInstrument("Client.activated",["BACKTRACE"]){Client.activated}
8
+ else
9
+ @clients = ::ScoutApm::AutoInstrument("Client.inactivated",["BACKTRACE"]){Client.inactivated}
10
+ end
11
+ end
12
+
13
+ def create
14
+ @client = ::ScoutApm::AutoInstrument("Client.new(params[:client])",["BACKTRACE"]){Client.new(params[:client])}
15
+ if ::ScoutApm::AutoInstrument("@client.save",["BACKTRACE"]){@client.save}
16
+ ::ScoutApm::AutoInstrument("redirect_to @client",["BACKTRACE"]){redirect_to @client}
17
+ else
18
+ # This line overrides the default rendering behavior, which
19
+ # would have been to render the "create" view.
20
+ ::ScoutApm::AutoInstrument("render \"new\"",["BACKTRACE"]){render "new"}
21
+ end
22
+ end
23
+
24
+ def edit
25
+ @client = ::ScoutApm::AutoInstrument("Client.new(params[:client])",["BACKTRACE"]){Client.new(params[:client])}
26
+
27
+ if ::ScoutApm::AutoInstrument("request.post?",["BACKTRACE"]){request.post?}
28
+ ::ScoutApm::AutoInstrument("@client.transaction do...",["BACKTRACE"]){@client.transaction do
29
+ @client.update_attributes(params[:client])
30
+ end}
31
+ end
32
+ end
33
+
34
+ def data
35
+ @clients = ::ScoutApm::AutoInstrument("Client.all",["BACKTRACE"]){Client.all}
36
+
37
+ formatter = ::ScoutApm::AutoInstrument("proc do |row|...",["BACKTRACE"]){proc do |row|
38
+ row.to_json
39
+ end}
40
+
41
+ ::ScoutApm::AutoInstrument("respond_with @clients.each(&formatter).join(\"\\n\"), :content_type => 'application/json; boundary=NL'",["BACKTRACE"]){respond_with @clients.each(&formatter).join("\n"), :content_type => 'application/json; boundary=NL'}
42
+ end
43
+
44
+ def things
45
+ x = {}
46
+ x[:this] ||= 'foo'
47
+ x[:that] &&= ::ScoutApm::AutoInstrument("'foo'.size",["BACKTRACE"]){'foo'.size}
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+
2
+ class ClientsController < ApplicationController
3
+ before_action :check_authorization
4
+
5
+ def index
6
+ if params[:status] == "activated"
7
+ @clients = Client.activated
8
+ else
9
+ @clients = Client.inactivated
10
+ end
11
+ end
12
+
13
+ def create
14
+ @client = Client.new(params[:client])
15
+ if @client.save
16
+ redirect_to @client
17
+ else
18
+ # This line overrides the default rendering behavior, which
19
+ # would have been to render the "create" view.
20
+ render "new"
21
+ end
22
+ end
23
+
24
+ def edit
25
+ @client = Client.new(params[:client])
26
+
27
+ if request.post?
28
+ @client.transaction do
29
+ @client.update_attributes(params[:client])
30
+ end
31
+ end
32
+ end
33
+
34
+ def data
35
+ @clients = Client.all
36
+
37
+ formatter = proc do |row|
38
+ row.to_json
39
+ end
40
+
41
+ respond_with @clients.each(&formatter).join("\n"), :content_type => 'application/json; boundary=NL'
42
+ end
43
+
44
+ def things
45
+ x = {}
46
+ x[:this] ||= 'foo'
47
+ x[:that] &&= 'foo'.size
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+
2
+ class BrokenController < ApplicationController
3
+ rescue_from Exception do |e|
4
+ if e.is_a? Pundit::NotAuthorizedError
5
+ unauthorized_error
6
+ elsif e.is_a? ActionController::ParameterMissing
7
+ error(status: 422, message: e.message)
8
+ else
9
+ log_error(e)
10
+ error message: 'Internal error', exception: e
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+
2
+ class BrokenController < ApplicationController
3
+ rescue_from Exception do |e|
4
+ if e.is_a? Pundit::NotAuthorizedError
5
+ unauthorized_error
6
+ elsif e.is_a? ActionController::ParameterMissing
7
+ error(status: 422, message: e.message)
8
+ else
9
+ log_error(e)
10
+ error message: 'Internal error', exception: e
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ require 'test_helper'
2
+
3
+ require 'scout_apm/auto_instrument'
4
+
5
+ class AutoInstrumentTest < Minitest::Test
6
+ def source_path(name)
7
+ File.expand_path("auto_instrument/#{name}.rb", __dir__)
8
+ end
9
+
10
+ def instrumented_path(name)
11
+ File.expand_path("auto_instrument/#{name}-instrumented.rb", __dir__)
12
+ end
13
+
14
+ def instrumented_source(name)
15
+ File.read(instrumented_path(name))
16
+ end
17
+
18
+ # Autoinstruments adds a backtrace to each created layer. This is the full path to the
19
+ # test controller.rb file, which will be different on different environments.
20
+ # This normalizes backtraces across environments.
21
+ def normalize_backtrace(string)
22
+ string.gsub(/\[".+auto_instrument\/.+?:.+?"\]/,'["BACKTRACE"]')
23
+ end
24
+
25
+ # Use this to automatically update the test fixtures.
26
+ def update_instrumented_source(name)
27
+ File.write(
28
+ instrumented_path(name),
29
+ normalize_backtrace(::ScoutApm::AutoInstrument::Rails.rewrite(source_path(name)))
30
+ )
31
+ end
32
+
33
+ def test_controller_rewrite
34
+ assert_equal instrumented_source("controller"),
35
+ normalize_backtrace(::ScoutApm::AutoInstrument::Rails.rewrite(source_path("controller")))
36
+
37
+ # update_instrumented_source("controller")
38
+ end
39
+
40
+ def test_rescue_from_rewrite
41
+ assert_equal instrumented_source("rescue_from"),
42
+ normalize_backtrace(::ScoutApm::AutoInstrument::Rails.rewrite(source_path("rescue_from")))
43
+
44
+ # update_instrumented_source("rescue_from")
45
+ end
46
+
47
+ def test_assignments_rewrite
48
+ assert_equal instrumented_source("assignments"),
49
+ normalize_backtrace(::ScoutApm::AutoInstrument::Rails.rewrite(source_path("assignments")))
50
+
51
+ # update_instrumented_source("assignments")
52
+ end
53
+ end if defined? ScoutApm::AutoInstrument
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: 2.5.3
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -109,6 +109,20 @@ dependencies:
109
109
  - - ">="
110
110
  - !ruby/object:Gem::Version
111
111
  version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: parser
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
112
126
  - !ruby/object:Gem::Dependency
113
127
  name: activerecord
114
128
  requirement: !ruby/object:Gem::Requirement
@@ -230,6 +244,11 @@ files:
230
244
  - lib/scout_apm/agent_context.rb
231
245
  - lib/scout_apm/app_server_load.rb
232
246
  - lib/scout_apm/attribute_arranger.rb
247
+ - lib/scout_apm/auto_instrument.rb
248
+ - lib/scout_apm/auto_instrument/instruction_sequence.rb
249
+ - lib/scout_apm/auto_instrument/layer.rb
250
+ - lib/scout_apm/auto_instrument/parser.rb
251
+ - lib/scout_apm/auto_instrument/rails.rb
233
252
  - lib/scout_apm/background_job_integrations/delayed_job.rb
234
253
  - lib/scout_apm/background_job_integrations/que.rb
235
254
  - lib/scout_apm/background_job_integrations/resque.rb
@@ -370,6 +389,14 @@ files:
370
389
  - test/test_helper.rb
371
390
  - test/tmp/README.md
372
391
  - test/unit/agent_test.rb
392
+ - test/unit/auto_instrument/assignments-instrumented.rb
393
+ - test/unit/auto_instrument/assignments.rb
394
+ - test/unit/auto_instrument/controller-ast.txt
395
+ - test/unit/auto_instrument/controller-instrumented.rb
396
+ - test/unit/auto_instrument/controller.rb
397
+ - test/unit/auto_instrument/rescue_from-instrumented.rb
398
+ - test/unit/auto_instrument/rescue_from.rb
399
+ - test/unit/auto_instrument_test.rb
373
400
  - test/unit/background_job_integrations/sidekiq_test.rb
374
401
  - test/unit/config_test.rb
375
402
  - test/unit/context_test.rb
@@ -430,7 +457,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
430
457
  - !ruby/object:Gem::Version
431
458
  version: '0'
432
459
  requirements: []
433
- rubygems_version: 3.0.4
460
+ rubygems_version: 3.0.6
434
461
  signing_key:
435
462
  specification_version: 4
436
463
  summary: Ruby application performance monitoring
@@ -439,6 +466,14 @@ test_files:
439
466
  - test/test_helper.rb
440
467
  - test/tmp/README.md
441
468
  - test/unit/agent_test.rb
469
+ - test/unit/auto_instrument/assignments-instrumented.rb
470
+ - test/unit/auto_instrument/assignments.rb
471
+ - test/unit/auto_instrument/controller-ast.txt
472
+ - test/unit/auto_instrument/controller-instrumented.rb
473
+ - test/unit/auto_instrument/controller.rb
474
+ - test/unit/auto_instrument/rescue_from-instrumented.rb
475
+ - test/unit/auto_instrument/rescue_from.rb
476
+ - test/unit/auto_instrument_test.rb
442
477
  - test/unit/background_job_integrations/sidekiq_test.rb
443
478
  - test/unit/config_test.rb
444
479
  - test/unit/context_test.rb