appmap 0.43.0 → 0.47.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.releaserc.yml +11 -0
- data/.travis.yml +33 -2
- data/CHANGELOG.md +44 -0
- data/README.md +66 -11
- data/README_CI.md +29 -0
- data/Rakefile +4 -2
- data/appmap.gemspec +5 -3
- data/lib/appmap.rb +3 -7
- data/lib/appmap/class_map.rb +11 -22
- data/lib/appmap/command/record.rb +1 -1
- data/lib/appmap/config.rb +180 -67
- data/lib/appmap/cucumber.rb +1 -1
- data/lib/appmap/event.rb +29 -28
- data/lib/appmap/handler/function.rb +19 -0
- data/lib/appmap/handler/net_http.rb +107 -0
- data/lib/appmap/handler/rails/request_handler.rb +124 -0
- data/lib/appmap/handler/rails/sql_handler.rb +152 -0
- data/lib/appmap/handler/rails/template.rb +149 -0
- data/lib/appmap/hook.rb +111 -70
- data/lib/appmap/hook/method.rb +6 -8
- data/lib/appmap/middleware/remote_recording.rb +1 -1
- data/lib/appmap/minitest.rb +22 -20
- data/lib/appmap/railtie.rb +5 -5
- data/lib/appmap/record.rb +1 -1
- data/lib/appmap/rspec.rb +22 -21
- data/lib/appmap/trace.rb +47 -6
- data/lib/appmap/util.rb +57 -2
- data/lib/appmap/version.rb +2 -2
- data/package-lock.json +3 -3
- data/release.sh +17 -0
- data/spec/abstract_controller_base_spec.rb +76 -15
- data/spec/class_map_spec.rb +5 -13
- data/spec/config_spec.rb +33 -1
- data/spec/fixtures/hook/custom_instance_method.rb +11 -0
- data/spec/fixtures/hook/method_named_call.rb +11 -0
- data/spec/hook_spec.rb +143 -22
- data/spec/record_net_http_spec.rb +160 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/util_spec.rb +18 -1
- data/test/expectations/openssl_test_key_sign1.json +2 -4
- data/test/gem_test.rb +1 -1
- data/test/rspec_test.rb +0 -13
- metadata +20 -14
- data/exe/appmap +0 -154
- data/lib/appmap/rails/request_handler.rb +0 -140
- data/lib/appmap/rails/sql_handler.rb +0 -150
- 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
|