appmap 0.42.1 → 0.46.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.
Files changed (64) 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 +74 -16
  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 +46 -27
  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 +47 -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 +140 -34
  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/fixtures/rails5_users_app/Gemfile +7 -3
  38. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  39. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  40. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  41. data/spec/fixtures/rails5_users_app/create_app +8 -2
  42. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  43. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  44. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  45. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  46. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  47. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  48. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  49. data/spec/fixtures/rails6_users_app/create_app +8 -2
  50. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  51. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  52. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  53. data/spec/hook_spec.rb +143 -22
  54. data/spec/record_net_http_spec.rb +160 -0
  55. data/spec/record_sql_rails_pg_spec.rb +1 -1
  56. data/spec/spec_helper.rb +16 -0
  57. data/test/expectations/openssl_test_key_sign1.json +2 -4
  58. data/test/gem_test.rb +1 -1
  59. data/test/rspec_test.rb +0 -13
  60. metadata +20 -14
  61. data/exe/appmap +0 -154
  62. data/lib/appmap/rails/request_handler.rb +0 -109
  63. data/lib/appmap/rails/sql_handler.rb +0 -150
  64. data/test/cli_test.rb +0 -116
@@ -1,109 +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
- class HTTPServerRequest < AppMap::Event::MethodEvent
10
- attr_accessor :normalized_path_info, :request_method, :path_info, :params
11
-
12
- def initialize(request)
13
- super AppMap::Event.next_id_counter, :call, Thread.current.object_id
14
-
15
- @request_method = request.request_method
16
- @normalized_path_info = normalized_path request
17
- @path_info = request.path_info.split('?')[0]
18
- # ActionDispatch::Http::ParameterFilter is deprecated
19
- parameter_filter_cls = \
20
- if defined?(ActiveSupport::ParameterFilter)
21
- ActiveSupport::ParameterFilter
22
- else
23
- ActionDispatch::Http::ParameterFilter
24
- end
25
- @params = parameter_filter_cls.new(::Rails.application.config.filter_parameters).filter(request.params)
26
- end
27
-
28
- def to_h
29
- super.tap do |h|
30
- h[:http_server_request] = {
31
- request_method: request_method,
32
- path_info: path_info,
33
- normalized_path_info: normalized_path_info
34
- }.compact
35
-
36
- h[:message] = params.keys.map do |key|
37
- val = params[key]
38
- {
39
- name: key,
40
- class: val.class.name,
41
- value: self.class.display_string(val),
42
- object_id: val.__id__
43
- }
44
- end
45
- end
46
- end
47
-
48
- private
49
-
50
- def normalized_path(request, router = ::Rails.application.routes.router)
51
- router.recognize request do |route, _|
52
- app = route.app
53
- next unless app.matches? request
54
- return normalized_path request, app.rack_app.routes.router if app.engine?
55
-
56
- return route.path.spec.to_s
57
- end
58
- end
59
- end
60
-
61
- class HTTPServerResponse < AppMap::Event::MethodReturnIgnoreValue
62
- attr_accessor :status, :mime_type
63
-
64
- def initialize(response, parent_id, elapsed)
65
- super AppMap::Event.next_id_counter, :return, Thread.current.object_id
66
-
67
- self.status = response.status
68
- self.mime_type = response.headers['Content-Type']
69
- self.parent_id = parent_id
70
- self.elapsed = elapsed
71
- end
72
-
73
- def to_h
74
- super.tap do |h|
75
- h[:http_server_response] = {
76
- status: status,
77
- mime_type: mime_type
78
- }.compact
79
- end
80
- end
81
- end
82
-
83
- class HookMethod < AppMap::Hook::Method
84
- def initialize
85
- # ActionController::Instrumentation has issued start_processing.action_controller and
86
- # process_action.action_controller since Rails 3. Therefore it's a stable place to hook
87
- # the request. Rails controller notifications can't be used directly because they don't
88
- # provide response headers, and we want the Content-Type.
89
- super(nil, ActionController::Instrumentation, ActionController::Instrumentation.instance_method(:process_action))
90
- end
91
-
92
- protected
93
-
94
- def before_hook(receiver, defined_class, _) # args
95
- call_event = HTTPServerRequest.new(receiver.request)
96
- # http_server_request events are i/o and do not require a package name.
97
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
98
- [ call_event, TIME_NOW.call ]
99
- end
100
-
101
- def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
102
- elapsed = TIME_NOW.call - start_time
103
- return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
104
- AppMap.tracing.record_event return_event
105
- end
106
- end
107
- end
108
- end
109
- 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