appmap 0.79.0 → 0.80.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: f5c6a40273c5606504492f151b2feeb1addc83d92f6bec7a80ca51d3034cd79f
4
- data.tar.gz: b10a582926396930d2a980cd4323bd83f7c3444aa0b77bf2dd2dfa632203f65d
3
+ metadata.gz: eb003b96b494282db035877377124bc3736d78320a64e479b7caa0b8df49063a
4
+ data.tar.gz: 3aab36a97c90485e1c7f77bed373c4c572b46a5c8c93ac4aa012db3becf201f4
5
5
  SHA512:
6
- metadata.gz: f56a49c05561a74ae2ee7512fbad2beadc1a0aa719e27b455ea330777c58e97b7c028f7eaaee13d52f20c8ace6bf9284ff8c717e17d922a45f2dca703781a737
7
- data.tar.gz: d35ac177b2b12ddd4f4e0517ed4101043c8e9d29f89f53721969d45e3e2be293ae7ad0b8466a2fae63329bb7a13e27d699b30a2464a663014d04a71a8d464293
6
+ metadata.gz: 8637610caacf145c5badb4de3eb399858a231d3de9a794f86e8fcb3d14801d3b5309453da7e9b799d47ddb9c4eed55bbd97bf2f77eabe7e605e95089b560c5bf
7
+ data.tar.gz: bba8d4f2f8ffa5b0e5d8b113657324561dfa71e7b7128273631d62d426c8e12339e305d9fb0fe0d05244685a1ddc1b4be4bfbe5a4b44607a2df96e5d60749557
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # [0.80.0](https://github.com/applandinc/appmap-ruby/compare/v0.79.0...v0.80.0) (2022-04-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Don't record SQL within an existing event ([ff37a69](https://github.com/applandinc/appmap-ruby/commit/ff37a69af1af02263df216e49aea0d0954b93925))
7
+
8
+
9
+ ### Features
10
+
11
+ * Env var to EXPLAIN queries ([740be75](https://github.com/applandinc/appmap-ruby/commit/740be75c2bc59e343d67ecf86b7715e61cddadba))
12
+ * Optionally record parameter schema ([b7f41b1](https://github.com/applandinc/appmap-ruby/commit/b7f41b15a4556a0ce78650a6a77301d365632bb8))
13
+ * query_plan is available whether a current transaction exists or not ([6edf774](https://github.com/applandinc/appmap-ruby/commit/6edf774fea858d825c4b971be2c4c15db1652446))
14
+ * Record parameter and return value size ([6e69754](https://github.com/applandinc/appmap-ruby/commit/6e697543cb421378832492e286f972dc4cb1e1aa))
15
+ * Save render return value to a thread-local ([f9d1e3f](https://github.com/applandinc/appmap-ruby/commit/f9d1e3f0aa9972482ff77233d38220104515b1d6))
16
+
1
17
  # [0.79.0](https://github.com/applandinc/appmap-ruby/compare/v0.78.0...v0.79.0) (2022-04-06)
2
18
 
3
19
 
data/lib/appmap/agent.rb CHANGED
@@ -100,5 +100,13 @@ module AppMap
100
100
  @metadata ||= Metadata.detect.freeze
101
101
  Util.deep_dup(@metadata)
102
102
  end
103
+
104
+ def parameter_schema?
105
+ ENV['APPMAP_PARAMETER_SCHEMA'] == 'true'
106
+ end
107
+
108
+ def explain_queries?
109
+ ENV['APPMAP_EXPLAIN_QUERIES'] == 'true'
110
+ end
103
111
  end
104
112
  end
data/lib/appmap/event.rb CHANGED
@@ -60,16 +60,18 @@ module AppMap
60
60
  final ? value_string : encode_display_string(value_string)
61
61
  end
62
62
 
63
- def object_properties(hash_like)
64
- hash = hash_like.to_h
65
- hash.keys.map do |key|
66
- {
67
- name: key,
68
- class: hash[key].class.name,
69
- }
63
+ def add_schema(param, value)
64
+ begin
65
+ if value.respond_to?(:keys)
66
+ param[:properties] = value.keys.map { |key| { name: key, class: best_class_name(value[key]) } }
67
+ elsif value.respond_to?(:first) && value.first
68
+ if value.first != value
69
+ add_schema param, value.first
70
+ end
71
+ end
72
+ rescue
73
+ warn "Error in add_schema(#{value.class})", $!
70
74
  end
71
- rescue
72
- nil
73
75
  end
74
76
 
75
77
  # Heuristic for dynamically defined class whose name can be nil
@@ -221,7 +223,9 @@ module AppMap
221
223
  object_id: value.__id__,
222
224
  value: display_string(value),
223
225
  kind: param_type
224
- }
226
+ }.tap do |param|
227
+ param[:size] = value.size if value.respond_to?(:size) && value.is_a?(Enumerable)
228
+ end
225
229
  end
226
230
  event.receiver = {
227
231
  class: best_class_name(receiver),
@@ -276,7 +280,7 @@ module AppMap
276
280
  attr_accessor :return_value, :exceptions
277
281
 
278
282
  class << self
279
- def build_from_invocation(parent_id, return_value, exception, elapsed: nil, event: MethodReturn.new)
283
+ def build_from_invocation(parent_id, return_value, exception, elapsed: nil, event: MethodReturn.new, parameter_schema: false)
280
284
  event ||= MethodReturn.new
281
285
  event.tap do |_|
282
286
  if return_value
@@ -284,7 +288,10 @@ module AppMap
284
288
  class: best_class_name(return_value),
285
289
  value: display_string(return_value),
286
290
  object_id: return_value.__id__
287
- }
291
+ }.tap do |param|
292
+ param[:size] = return_value.size if return_value.respond_to?(:size) && return_value.is_a?(Enumerable)
293
+ add_schema param, return_value if parameter_schema && !exception
294
+ end
288
295
  end
289
296
  if exception
290
297
  next_exception = exception
@@ -43,3 +43,9 @@
43
43
  - ActionController::Instrumentation#redirect_to
44
44
  label: mvc.controller
45
45
  require_name: action_controller
46
+ - methods:
47
+ - AbstractController::Rendering#render_to_body
48
+ - ActionController::Renderers#render_to_body
49
+ label: mvc.render
50
+ handler_class: AppMap::Handler::Rails::RenderHandler
51
+ require_name: action_controller
@@ -0,0 +1,29 @@
1
+ require 'appmap/handler/function'
2
+
3
+ module AppMap
4
+ module Handler
5
+ module Rails
6
+ class RenderHandler < AppMap::Handler::Function
7
+ def handle_call(receiver, args)
8
+ options, _ = args
9
+ # TODO: :file, :xml
10
+ # https://guides.rubyonrails.org/v5.1/layouts_and_rendering.html
11
+ if options[:json]
12
+ Thread.current[TEMPLATE_RENDER_FORMAT] = :json
13
+ end
14
+
15
+ super
16
+ end
17
+
18
+ def handle_return(call_event_id, elapsed, return_value, exception)
19
+ if Thread.current[TEMPLATE_RENDER_FORMAT] == :json
20
+ Thread.current[TEMPLATE_RENDER_VALUE] = JSON.parse(return_value) rescue nil
21
+ end
22
+ Thread.current[TEMPLATE_RENDER_FORMAT] = nil
23
+
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -7,6 +7,7 @@ require 'appmap/util'
7
7
  module AppMap
8
8
  module Handler
9
9
  module Rails
10
+
10
11
  module RequestHandler
11
12
  class HTTPServerRequest < AppMap::Event::MethodEvent
12
13
  attr_accessor :normalized_path_info, :request_method, :path_info, :params, :headers
@@ -46,8 +47,7 @@ module AppMap
46
47
  value: self.class.display_string(val),
47
48
  object_id: val.__id__,
48
49
  }.tap do |message|
49
- properties = object_properties(val)
50
- message[:properties] = properties if properties
50
+ AppMap::Event::MethodEvent.add_schema message, val
51
51
  end
52
52
  end
53
53
  end
@@ -67,16 +67,16 @@ module AppMap
67
67
  end
68
68
  end
69
69
 
70
- class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
70
+ class HTTPServerResponse < AppMap::Event::MethodReturn
71
71
  attr_accessor :status, :headers
72
72
 
73
- def initialize(response, parent_id, elapsed)
74
- super AppMap::Event.next_id_counter, :return, Thread.current.object_id
75
-
76
- self.status = response.status
77
- self.parent_id = parent_id
78
- self.elapsed = elapsed
79
- self.headers = response.headers.dup
73
+ class << self
74
+ def build_from_invocation(parent_id, return_value, elapsed, response, event: HTTPServerResponse.new)
75
+ event ||= HTTPServerResponse.new
76
+ event.status = response.status
77
+ event.headers = response.headers.dup
78
+ AppMap::Event::MethodReturn.build_from_invocation parent_id, return_value, nil, elapsed: elapsed, event: event, parameter_schema: true
79
+ end
80
80
  end
81
81
 
82
82
  def to_h
@@ -108,7 +108,9 @@ module AppMap
108
108
  end
109
109
 
110
110
  def after_hook(receiver, call_event, elapsed, *)
111
- return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
111
+ return_value = Thread.current[TEMPLATE_RENDER_VALUE]
112
+ Thread.current[TEMPLATE_RENDER_VALUE] = nil
113
+ return_event = HTTPServerResponse.build_from_invocation call_event.id, return_value, elapsed, receiver.response
112
114
  AppMap.tracing.record_event return_event
113
115
  end
114
116
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'appmap/event'
4
+ require 'appmap/hook/method'
4
5
 
5
6
  module AppMap
6
7
  module Handler
@@ -21,6 +22,7 @@ module AppMap
21
22
  sql: payload[:sql],
22
23
  database_type: payload[:database_type]
23
24
  }.tap do |sql_query|
25
+ sql_query[:query_plan] = payload[:query_plan] if payload[:query_plan]
24
26
  %i[server_version].each do |attribute|
25
27
  sql_query[attribute] = payload[attribute] if payload[attribute]
26
28
  end
@@ -43,6 +45,36 @@ module AppMap
43
45
  def examine(payload, sql:)
44
46
  return unless (examiner = build_examiner)
45
47
 
48
+ in_transaction = examiner.in_transaction?
49
+
50
+ if AppMap.explain_queries? && examiner.database_type == :postgres
51
+ if sql =~ /\A(SELECT|INSERT|UPDATE|DELETE|WITH)/i
52
+ savepoint_established = \
53
+ begin
54
+ tx_query = in_transaction ? 'SAVEPOINT appmap_sql_examiner' : 'BEGIN TRANSACTION'
55
+ examiner.execute_query tx_query
56
+ true
57
+ rescue
58
+ # Probably: Sequel::DatabaseError: PG::InFailedSqlTransaction
59
+ warn $!
60
+ false
61
+ end
62
+
63
+ if savepoint_established
64
+ plan = nil
65
+ begin
66
+ plan = examiner.execute_query(%(EXPLAIN #{sql}))
67
+ payload[:query_plan] = plan.map { |line| line[:'QUERY PLAN'] }.join("\n")
68
+ rescue
69
+ warn "(appmap) Error explaining query: #{$!}"
70
+ ensure
71
+ tx_query = in_transaction ? 'ROLLBACK TO SAVEPOINT appmap_sql_examiner' : 'ROLLBACK'
72
+ examiner.execute_query tx_query
73
+ end
74
+ end
75
+ end
76
+ end
77
+
46
78
  payload[:server_version] = examiner.server_version
47
79
  payload[:database_type] = examiner.database_type.to_s
48
80
  end
@@ -67,6 +99,10 @@ module AppMap
67
99
  Sequel::Model.db.database_type.to_sym
68
100
  end
69
101
 
102
+ def in_transaction?
103
+ Sequel::Model.db.in_transaction?
104
+ end
105
+
70
106
  def execute_query(sql)
71
107
  Sequel::Model.db[sql].all
72
108
  end
@@ -93,8 +129,12 @@ module AppMap
93
129
  type
94
130
  end
95
131
 
132
+ def in_transaction?
133
+ ActiveRecord::Base.connection.open_transactions > 0
134
+ end
135
+
96
136
  def execute_query(sql)
97
- ActiveRecord::Base.connection.execute(sql).inject([]) { |memo, r| memo << r; memo }
137
+ ActiveRecord::Base.connection.execute(sql).to_a
98
138
  end
99
139
  end
100
140
  end
@@ -102,6 +142,8 @@ module AppMap
102
142
  def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
103
143
  return if AppMap.tracing.empty?
104
144
 
145
+ return if Thread.current[AppMap::Hook::Method::HOOK_DISABLE_KEY] == true
146
+
105
147
  reentry_key = "#{self.class.name}#call"
106
148
  return if Thread.current[reentry_key] == true
107
149
 
@@ -5,6 +5,9 @@ require 'active_support/inflector/methods'
5
5
  module AppMap
6
6
  # Specific hook handler classes and general related utilities.
7
7
  module Handler
8
+ TEMPLATE_RENDER_FORMAT = 'appmap.handler.template.return_value_format'
9
+ TEMPLATE_RENDER_VALUE = 'appmap.handler.template.return_value'
10
+
8
11
  # Try to find handler module with a given name.
9
12
  #
10
13
  # If the module is not loaded, tries to require the appropriate file
@@ -24,7 +24,6 @@ module AppMap
24
24
  attr_reader :hook_package, :hook_class, :hook_method, :parameters, :arity
25
25
 
26
26
  HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
27
- private_constant :HOOK_DISABLE_KEY
28
27
 
29
28
  def initialize(hook_package, hook_class, hook_method)
30
29
  @hook_package = hook_package
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.79.0'
6
+ VERSION = '0.80.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
@@ -32,7 +32,7 @@ describe 'AppMap::Handler::Eval' do
32
32
  expect(events[0]).to match hash_including \
33
33
  defined_class: 'Kernel',
34
34
  method_id: 'eval',
35
- parameters: [{ class: 'Array', kind: :rest, name: 'arg', value: '[12]' }]
35
+ parameters: [{ class: 'Array', kind: :rest, name: 'arg', size: 1, value: '[12]' }]
36
36
  end
37
37
 
38
38
  # a la Ruby 2.6.3 ruby-token.rb
data/spec/hook_spec.rb CHANGED
@@ -491,6 +491,7 @@ describe 'AppMap class Hooking' do
491
491
  :class: Array
492
492
  :value: "[4, 5]"
493
493
  :kind: :rest
494
+ :size: 2
494
495
  - :name: :kw1
495
496
  :class: String
496
497
  :value: one
@@ -503,6 +504,7 @@ describe 'AppMap class Hooking' do
503
504
  :class: Hash
504
505
  :value: "{:kw3=>:three}"
505
506
  :kind: :keyrest
507
+ :size: 1
506
508
  :receiver:
507
509
  :class: InstanceMethod
508
510
  :value: Instance Method fixture
@@ -1139,6 +1141,7 @@ describe 'AppMap class Hooking' do
1139
1141
  :class: Array
1140
1142
  :value: "[foo]"
1141
1143
  :kind: :rest
1144
+ :size: 1
1142
1145
  - :name: :kw1
1143
1146
  :class: String
1144
1147
  :value: kw1
@@ -1151,6 +1154,7 @@ describe 'AppMap class Hooking' do
1151
1154
  :class: Hash
1152
1155
  :value: "{}"
1153
1156
  :kind: :keyrest
1157
+ :size: 0
1154
1158
  :receiver:
1155
1159
  :class: ReportParameters
1156
1160
  :value: ReportParameters
@@ -1160,6 +1164,7 @@ describe 'AppMap class Hooking' do
1160
1164
  :return_value:
1161
1165
  :class: Array
1162
1166
  :value: "[[:rest, :args], [:keyreq, :kw1], [:key, :kw2], [:keyrest, :kws]]"
1167
+ :size: 4
1163
1168
  YAML
1164
1169
  parameters = [[:rest, :args], [:keyreq, :kw1], [:key, :kw2], [:keyrest, :kws]]
1165
1170
  test_hook_behavior 'spec/fixtures/hook/report_parameters.rb', events do
@@ -54,7 +54,8 @@ describe 'Rails' do
54
54
  'http_server_response' => hash_including(
55
55
  'status_code' => 201,
56
56
  'headers' => hash_including('Content-Type' => 'application/json; charset=utf-8'),
57
- )
57
+ ),
58
+ 'return_value' => hash_including('class' => 'Hash', 'object_id' => Integer, 'properties' => include({'name' => 'login', 'class' => 'String'})),
58
59
  )
59
60
  )
60
61
  end
@@ -72,6 +73,7 @@ describe 'Rails' do
72
73
  'name' => 'params',
73
74
  'class' => 'ActiveSupport::HashWithIndifferentAccess',
74
75
  'object_id' => Integer,
76
+ 'size' => 1,
75
77
  'value' => '{login=>alice}',
76
78
  'kind' => 'req'
77
79
  ),
@@ -26,7 +26,8 @@
26
26
  "name": "arg",
27
27
  "class": "Array",
28
28
  "value": "[e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855, the document]",
29
- "kind": "rest"
29
+ "kind": "rest",
30
+ "size": 2
30
31
  }
31
32
  ],
32
33
  "receiver": {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appmap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.79.0
4
+ version: 0.80.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gilpin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-06 00:00:00.000000000 Z
11
+ date: 2022-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -394,6 +394,7 @@ files:
394
394
  - lib/appmap/handler/eval.rb
395
395
  - lib/appmap/handler/function.rb
396
396
  - lib/appmap/handler/net_http.rb
397
+ - lib/appmap/handler/rails/render_handler.rb
397
398
  - lib/appmap/handler/rails/request_handler.rb
398
399
  - lib/appmap/handler/rails/sql_handler.rb
399
400
  - lib/appmap/handler/rails/template.rb