appmap 0.43.0 → 0.47.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +33 -2
  4. data/CHANGELOG.md +44 -0
  5. data/README.md +66 -11
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +3 -7
  10. data/lib/appmap/class_map.rb +11 -22
  11. data/lib/appmap/command/record.rb +1 -1
  12. data/lib/appmap/config.rb +180 -67
  13. data/lib/appmap/cucumber.rb +1 -1
  14. data/lib/appmap/event.rb +29 -28
  15. data/lib/appmap/handler/function.rb +19 -0
  16. data/lib/appmap/handler/net_http.rb +107 -0
  17. data/lib/appmap/handler/rails/request_handler.rb +124 -0
  18. data/lib/appmap/handler/rails/sql_handler.rb +152 -0
  19. data/lib/appmap/handler/rails/template.rb +149 -0
  20. data/lib/appmap/hook.rb +111 -70
  21. data/lib/appmap/hook/method.rb +6 -8
  22. data/lib/appmap/middleware/remote_recording.rb +1 -1
  23. data/lib/appmap/minitest.rb +22 -20
  24. data/lib/appmap/railtie.rb +5 -5
  25. data/lib/appmap/record.rb +1 -1
  26. data/lib/appmap/rspec.rb +22 -21
  27. data/lib/appmap/trace.rb +47 -6
  28. data/lib/appmap/util.rb +57 -2
  29. data/lib/appmap/version.rb +2 -2
  30. data/package-lock.json +3 -3
  31. data/release.sh +17 -0
  32. data/spec/abstract_controller_base_spec.rb +76 -15
  33. data/spec/class_map_spec.rb +5 -13
  34. data/spec/config_spec.rb +33 -1
  35. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  36. data/spec/fixtures/hook/method_named_call.rb +11 -0
  37. data/spec/hook_spec.rb +143 -22
  38. data/spec/record_net_http_spec.rb +160 -0
  39. data/spec/spec_helper.rb +10 -0
  40. data/spec/util_spec.rb +18 -1
  41. data/test/expectations/openssl_test_key_sign1.json +2 -4
  42. data/test/gem_test.rb +1 -1
  43. data/test/rspec_test.rb +0 -13
  44. metadata +20 -14
  45. data/exe/appmap +0 -154
  46. data/lib/appmap/rails/request_handler.rb +0 -140
  47. data/lib/appmap/rails/sql_handler.rb +0 -150
  48. data/test/cli_test.rb +0 -116
@@ -1,140 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'appmap/event'
4
- require 'appmap/hook'
5
-
6
- module AppMap
7
- module Rails
8
- module RequestHandler
9
- # Host and User-Agent will just introduce needless variation.
10
- # Content-Type and Authorization get their own fields in the request.
11
- IGNORE_HEADERS = %w[host user_agent content_type authorization].map(&:upcase).map {|h| "HTTP_#{h}"}.freeze
12
-
13
- class << self
14
- def selected_headers(env)
15
- # Rack prepends HTTP_ to all client-sent headers.
16
- matching_headers = env
17
- .select { |k,v| k.start_with? 'HTTP_'}
18
- .reject { |k,v| IGNORE_HEADERS.member?(k) }
19
- .reject { |k,v| v.blank? }
20
- .each_with_object({}) do |kv, memo|
21
- key = kv[0].sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
22
- value = kv[1]
23
- memo[key] = value
24
- end
25
- matching_headers.blank? ? nil : matching_headers
26
- end
27
- end
28
-
29
- class HTTPServerRequest < AppMap::Event::MethodEvent
30
- attr_accessor :normalized_path_info, :request_method, :path_info, :params, :mime_type, :headers, :authorization
31
-
32
- def initialize(request)
33
- super AppMap::Event.next_id_counter, :call, Thread.current.object_id
34
-
35
- self.request_method = request.request_method
36
- self.normalized_path_info = normalized_path(request)
37
- self.mime_type = request.headers['Content-Type']
38
- self.headers = RequestHandler.selected_headers(request.env)
39
- self.authorization = request.headers['Authorization']
40
- self.path_info = request.path_info.split('?')[0]
41
- # ActionDispatch::Http::ParameterFilter is deprecated
42
- parameter_filter_cls = \
43
- if defined?(ActiveSupport::ParameterFilter)
44
- ActiveSupport::ParameterFilter
45
- else
46
- ActionDispatch::Http::ParameterFilter
47
- end
48
- self.params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
49
- end
50
-
51
- def to_h
52
- super.tap do |h|
53
- h[:http_server_request] = {
54
- request_method: request_method,
55
- path_info: path_info,
56
- mime_type: mime_type,
57
- normalized_path_info: normalized_path_info,
58
- authorization: authorization,
59
- headers: headers,
60
- }.compact
61
-
62
- h[:message] = params.keys.map do |key|
63
- val = params[key]
64
- {
65
- name: key,
66
- class: val.class.name,
67
- value: self.class.display_string(val),
68
- object_id: val.__id__,
69
- }.tap do |message|
70
- properties = object_properties(val)
71
- message[:properties] = properties if properties
72
- end
73
- end
74
- end
75
- end
76
-
77
- private
78
-
79
- def normalized_path(request, router = ::Rails.application.routes.router)
80
- router.recognize request do |route, _|
81
- app = route.app
82
- next unless app.matches? request
83
- return normalized_path request, app.rack_app.routes.router if app.engine?
84
-
85
- return route.path.spec.to_s
86
- end
87
- end
88
- end
89
-
90
- class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
91
- attr_accessor :status, :mime_type, :headers
92
-
93
- def initialize(response, parent_id, elapsed)
94
- super AppMap::Event.next_id_counter, :return, Thread.current.object_id
95
-
96
- self.status = response.status
97
- self.mime_type = response.headers['Content-Type']
98
- self.parent_id = parent_id
99
- self.elapsed = elapsed
100
- self.headers = RequestHandler.selected_headers(response.headers)
101
- end
102
-
103
- def to_h
104
- super.tap do |h|
105
- h[:http_server_response] = {
106
- status: status,
107
- mime_type: mime_type,
108
- headers: headers
109
- }.compact
110
- end
111
- end
112
- end
113
-
114
- class HookMethod < AppMap::Hook::Method
115
- def initialize
116
- # ActionController::Instrumentation has issued start_processing.action_controller and
117
- # process_action.action_controller since Rails 3. Therefore it's a stable place to hook
118
- # the request. Rails controller notifications can't be used directly because they don't
119
- # provide response headers, and we want the Content-Type.
120
- super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
121
- end
122
-
123
- protected
124
-
125
- def before_hook(receiver, defined_class, _) # args
126
- call_event = HTTPServerRequest.new(receiver.request)
127
- # http_server_request events are i/o and do not require a package name.
128
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
129
- [ call_event, TIME_NOW.call ]
130
- end
131
-
132
- def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
133
- elapsed = TIME_NOW.call - start_time
134
- return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
135
- AppMap.tracing.record_event return_event
136
- end
137
- end
138
- end
139
- end
140
- end
@@ -1,150 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'appmap/event'
4
-
5
- module AppMap
6
- module Rails
7
- class SQLHandler
8
- class SQLCall < AppMap::Event::MethodCall
9
- attr_accessor :payload
10
-
11
- def initialize(payload)
12
- super AppMap::Event.next_id_counter, :call, Thread.current.object_id
13
-
14
- self.payload = payload
15
- end
16
-
17
- def to_h
18
- super.tap do |h|
19
- h[:sql_query] = {
20
- sql: payload[:sql],
21
- database_type: payload[:database_type]
22
- }.tap do |sql_query|
23
- %i[server_version].each do |attribute|
24
- sql_query[attribute] = payload[attribute] if payload[attribute]
25
- end
26
- end
27
- end
28
- end
29
- end
30
-
31
- class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
32
- def initialize(parent_id, elapsed)
33
- super AppMap::Event.next_id_counter, :return, Thread.current.object_id
34
-
35
- self.parent_id = parent_id
36
- self.elapsed = elapsed
37
- end
38
- end
39
-
40
- module SQLExaminer
41
- class << self
42
- def examine(payload, sql:)
43
- return unless (examiner = build_examiner)
44
-
45
- payload[:server_version] = examiner.server_version
46
- payload[:database_type] = examiner.database_type.to_s
47
- end
48
-
49
- protected
50
-
51
- def build_examiner
52
- if defined?(Sequel)
53
- SequelExaminer.new
54
- elsif defined?(ActiveRecord)
55
- ActiveRecordExaminer.new
56
- end
57
- end
58
- end
59
-
60
- class SequelExaminer
61
- def server_version
62
- Sequel::Model.db.server_version
63
- end
64
-
65
- def database_type
66
- Sequel::Model.db.database_type.to_sym
67
- end
68
-
69
- def execute_query(sql)
70
- Sequel::Model.db[sql].all
71
- end
72
- end
73
-
74
- class ActiveRecordExaminer
75
- @@db_version_warning_issued = {}
76
-
77
- def issue_warning
78
- db_type = database_type
79
- return if @@db_version_warning_issued[db_type]
80
- warn("AppMap: Unable to determine database version for #{db_type.inspect}")
81
- @@db_version_warning_issued[db_type] = true
82
- end
83
-
84
- def server_version
85
- ActiveRecord::Base.connection.try(:database_version) || issue_warning
86
- end
87
-
88
- def database_type
89
- type = ActiveRecord::Base.connection.adapter_name.downcase.to_sym
90
- type = :postgres if type == :postgresql
91
-
92
- type
93
- end
94
-
95
- def execute_query(sql)
96
- ActiveRecord::Base.connection.execute(sql).inject([]) { |memo, r| memo << r; memo }
97
- end
98
- end
99
- end
100
-
101
- def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
102
- return if AppMap.tracing.empty?
103
-
104
- reentry_key = "#{self.class.name}#call"
105
- return if Thread.current[reentry_key] == true
106
-
107
- Thread.current[reentry_key] = true
108
- begin
109
- sql = payload[:sql].strip
110
-
111
- # Detect whether a function call within a specified filename is present in the call stack.
112
- find_in_backtrace = lambda do |file_name, function_name = nil|
113
- Thread.current.backtrace.find do |line|
114
- tokens = line.split(':')
115
- matches_file = tokens.find { |t| t.rindex(file_name) == (t.length - file_name.length) }
116
- matches_function = function_name.nil? || tokens.find { |t| t == "in `#{function_name}'" }
117
- matches_file && matches_function
118
- end
119
- end
120
-
121
- # Ignore SQL calls which are made while establishing a new connection.
122
- #
123
- # Example:
124
- # /path/to/ruby/2.6.0/gems/sequel-5.20.0/lib/sequel/connection_pool.rb:122:in `make_new'
125
- return if find_in_backtrace.call('lib/sequel/connection_pool.rb', 'make_new')
126
- # lib/active_record/connection_adapters/abstract/connection_pool.rb:811:in `new_connection'
127
- return if find_in_backtrace.call('lib/active_record/connection_adapters/abstract/connection_pool.rb', 'new_connection')
128
-
129
- # Ignore SQL calls which are made while inspecting the DB schema.
130
- #
131
- # Example:
132
- # /path/to/ruby/2.6.0/gems/sequel-5.20.0/lib/sequel/model/base.rb:812:in `get_db_schema'
133
- return if find_in_backtrace.call('lib/sequel/model/base.rb', 'get_db_schema')
134
- # /usr/local/bundle/gems/activerecord-5.2.3/lib/active_record/model_schema.rb:466:in `load_schema!'
135
- return if find_in_backtrace.call('lib/active_record/model_schema.rb', 'load_schema!')
136
- return if find_in_backtrace.call('lib/active_model/attribute_methods.rb', 'define_attribute_methods')
137
- return if find_in_backtrace.call('lib/active_record/connection_adapters/schema_cache.rb')
138
-
139
- SQLExaminer.examine payload, sql: sql
140
-
141
- call = SQLCall.new(payload)
142
- AppMap.tracing.record_event(call)
143
- AppMap.tracing.record_event(SQLReturn.new(call.id, finished - started))
144
- ensure
145
- Thread.current[reentry_key] = nil
146
- end
147
- end
148
- end
149
- end
150
- end
data/test/cli_test.rb DELETED
@@ -1,116 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'test_helper'
5
- require 'English'
6
-
7
- class CLITest < Minitest::Test
8
- OUTPUT_FILENAME = File.expand_path('../tmp/appmap.json', __dir__)
9
- STATS_OUTPUT_FILENAME = File.expand_path('../tmp/stats.txt', __dir__)
10
-
11
- def setup
12
- FileUtils.rm_f OUTPUT_FILENAME
13
- FileUtils.rm_f STATS_OUTPUT_FILENAME
14
- end
15
-
16
- def test_record
17
- output = Dir.chdir 'test/fixtures/cli_record_test' do
18
- `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
19
- end
20
-
21
- assert_equal 0, $CHILD_STATUS.exitstatus
22
- assert File.file?(OUTPUT_FILENAME), "#{OUTPUT_FILENAME} does not exist"
23
- assert_equal 'Hello', output
24
- output = JSON.parse(File.read(OUTPUT_FILENAME))
25
- assert output['classMap'], 'Output should contain classMap'
26
- assert output['events'], 'Output should contain events'
27
- end
28
-
29
- def test_stats_to_file
30
- Dir.chdir 'test/fixtures/cli_record_test' do
31
- `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
32
- end
33
- assert_equal 0, $CHILD_STATUS.exitstatus
34
-
35
- output = Dir.chdir 'test/fixtures/cli_record_test' do
36
- `#{File.expand_path '../exe/appmap', __dir__} stats -o #{STATS_OUTPUT_FILENAME} #{OUTPUT_FILENAME}`.strip
37
- end
38
- assert_equal 0, $CHILD_STATUS.exitstatus
39
- assert_equal '', output
40
- assert File.file?(OUTPUT_FILENAME), "#{OUTPUT_FILENAME} does not exist"
41
- end
42
-
43
-
44
- def test_stats_text
45
- Dir.chdir 'test/fixtures/cli_record_test' do
46
- `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
47
- end
48
- assert_equal 0, $CHILD_STATUS.exitstatus
49
-
50
- output = Dir.chdir 'test/fixtures/cli_record_test' do
51
- `#{File.expand_path '../exe/appmap', __dir__} stats -o - #{OUTPUT_FILENAME}`.strip
52
- end
53
-
54
- assert_equal 0, $CHILD_STATUS.exitstatus
55
- assert_equal <<~OUTPUT.strip, output.strip
56
- Class frequency:
57
- ----------------
58
- 1 Main
59
-
60
- Method frequency:
61
- ----------------
62
- 1 Main.say_hello
63
- OUTPUT
64
- end
65
-
66
- def test_stats_json
67
- Dir.chdir 'test/fixtures/cli_record_test' do
68
- `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
69
- end
70
- assert_equal 0, $CHILD_STATUS.exitstatus
71
-
72
- output = Dir.chdir 'test/fixtures/cli_record_test' do
73
- `#{File.expand_path '../exe/appmap', __dir__} stats -f json -o - #{OUTPUT_FILENAME}`.strip
74
- end
75
-
76
- assert_equal 0, $CHILD_STATUS.exitstatus
77
- assert_equal <<~OUTPUT.strip, output.strip
78
- {
79
- "class_frequency": [
80
- {
81
- "name": "Main",
82
- "count": 1
83
- }
84
- ],
85
- "method_frequency": [
86
- {
87
- "name": "Main.say_hello",
88
- "count": 1
89
- }
90
- ]
91
- }
92
- OUTPUT
93
- end
94
-
95
- def test_record_to_default_location
96
- Dir.chdir 'test/fixtures/cli_record_test' do
97
- system({ 'APPMAP_FILE' => OUTPUT_FILENAME }, "#{File.expand_path '../exe/appmap', __dir__} record ./lib/cli_record_test/main.rb")
98
- end
99
-
100
- assert_equal 0, $CHILD_STATUS.exitstatus
101
- assert File.file?(OUTPUT_FILENAME), 'appmap.json does not exist'
102
- end
103
-
104
- def test_record_to_stdout
105
- output = Dir.chdir 'test/fixtures/cli_record_test' do
106
- `#{File.expand_path '../exe/appmap', __dir__} record -o - ./lib/cli_record_test/main.rb`
107
- end
108
-
109
- assert_equal 0, $CHILD_STATUS.exitstatus
110
- # Event path
111
- assert_includes output, %("path":"lib/cli_record_test/main.rb")
112
- # Function location
113
- assert_includes output, %("location":"lib/cli_record_test/main.rb:3")
114
- assert !File.file?(OUTPUT_FILENAME), "#{OUTPUT_FILENAME} should not exist"
115
- end
116
- end