scout_apm 2.5.3 → 2.6.0

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