appmap 0.34.4 → 0.36.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3a509a94faa2a85ff11702ae55ab3df975f04416d93d169b0a24989fd520b9d
4
- data.tar.gz: 3895b9c788413f85738ba487e717bf48e4dfc2e8c0792f30c6f9f6ba7dc5e9c8
3
+ metadata.gz: 102dab80f2d8cffd51fd1377583a1b063078cd77ab7ad4ffdcd20867a084b11b
4
+ data.tar.gz: cbb87144593416f84099cf8a9d8ac18f18d9763dce28277320ae6a99ff59ec58
5
5
  SHA512:
6
- metadata.gz: 07fe201395cad27e731402b5b2ff0efd653056dad16b9301657034df0b6e7fd3a205768ef3656874f55109edfc4d788d752f8bc73f4ff6874c815104547ce281
7
- data.tar.gz: cbbd1551c352519954e9ddc9dd27279d6d232663fb67fd5ed4369a603a382f140593aa195aa08f35a57b0a14d38bef07bfef0aa7bd942d660a865fb3682d500d
6
+ metadata.gz: a24e2ecb22a654d11e91ef28a7fdd961c178144f474a36e26c70e8e243bac18c13e3c5ff606f5ed2a058ddf6d98f9dbd0678a0f986ddfae4f18ed7524c27d41d
7
+ data.tar.gz: 712cb7e18026aa80135af7ce54a7a8c412362355c0eb6b151a0f05354cf991ebe9402e298d9646b9fab2e5d673d3338a773a0b1b1d0e1846ec3ed970074a1f77
@@ -1,3 +1,27 @@
1
+ # v0.36.0
2
+ * *appmap.yml* package definition may specify `gem`.
3
+ * Skip loading the railtie if `APPMAP_INITIALIZE` environment variable
4
+ is set to `false`.
5
+
6
+ # v0.35.2
7
+ * Make sure `MethodEvent#display_string` works when the value's `#to_s` and/or `#inspect`
8
+ methods have problems.
9
+
10
+ # v0.35.1
11
+ * Take out hooking of `IO` and `Logger` methods.
12
+ * Enable logging if either `APPMAP_DEBUG` or `DEBUG` is `true`.
13
+
14
+ # v0.35.0
15
+ * Provide a custom display string for files and HTTP requests.
16
+ * Report `mime_type` on HTTP response.
17
+
18
+ # v0.34.6
19
+ * Only warn once about problems determining database version for an ActiveRecord
20
+ connection.
21
+
22
+ # v0.34.5
23
+ * Ensure that hooking a method doesn't change its arity.
24
+
1
25
  # v0.34.4
2
26
  * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
3
27
  `ActiveRecord::Base.connection.database_version` if it's available.
data/README.md CHANGED
@@ -57,6 +57,15 @@ end
57
57
 
58
58
  Then install with `bundle`.
59
59
 
60
+ **Railtie**
61
+
62
+ If you are using Ruby on Rails, require the railtie after Rails is loaded.
63
+
64
+ ```
65
+ # application.rb is a good place to do this, along with all the other railties.
66
+ require 'appmap/railtie'
67
+ ```
68
+
60
69
  # Configuration
61
70
 
62
71
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -67,6 +76,7 @@ name: MyProject
67
76
  packages:
68
77
  - path: app/controllers
69
78
  - path: app/models
79
+ - gem: activerecord
70
80
  ```
71
81
 
72
82
  * **name** Provides the project name (required)
@@ -78,6 +88,7 @@ Each entry in the `packages` list is a YAML object which has the following keys:
78
88
 
79
89
  * **path** The path to the source code directory. The path may be relative to the current working directory, or it may
80
90
  be an absolute path.
91
+ * **gem** As an alternative to specifying the path, specify the name of a dependency gem. When using `gem`, don't specify `path`.
81
92
  * **exclude** A list of files and directories which will be ignored. By default, all modules, classes and public
82
93
  functions are inspected.
83
94
 
data/Rakefile CHANGED
@@ -113,7 +113,7 @@ namespace :spec do
113
113
  desc ruby_version
114
114
  task ruby_version, [:specs] => ["compile", "build:fixtures:#{ruby_version}:all"] do |_, task_args|
115
115
  run_specs(ruby_version, task_args)
116
- end.tap do|t|
116
+ end.tap do |t|
117
117
  desc "Run all specs"
118
118
  task :all, [:specs] => t
119
119
  end
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'parser'
31
31
  spec.add_dependency 'rack'
32
32
 
33
- spec.add_development_dependency 'bundler', '~> 1.16'
33
+ spec.add_development_dependency 'bundler', '>= 1.16'
34
34
  spec.add_development_dependency 'minitest', '~> 5.0'
35
35
  spec.add_development_dependency 'pry-byebug'
36
36
  spec.add_development_dependency 'rake', '>= 12.3.3'
data/appmap.yml CHANGED
@@ -1,8 +1,2 @@
1
1
  name: AppMap Rubygem
2
- packages:
3
- - path: lib/appmap
4
- exclude:
5
- - server
6
- - trace
7
-
8
- - path: examples
2
+ packages: []
@@ -8,7 +8,12 @@
8
8
  (!SPECIAL_CONST_P(obj) && \
9
9
  (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE))
10
10
 
11
- static VALUE singleton_method_owner_name(VALUE klass, VALUE method)
11
+ #define ARITIES_KEY "__arities__"
12
+
13
+ VALUE am_AppMapHook;
14
+
15
+ static VALUE
16
+ singleton_method_owner_name(VALUE klass, VALUE method)
12
17
  {
13
18
  VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
14
19
  VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
@@ -27,10 +32,64 @@ static VALUE singleton_method_owner_name(VALUE klass, VALUE method)
27
32
  // #to_s on the method's owner and hope for the best.
28
33
  return rb_funcall(owner, rb_intern("to_s"), 0);
29
34
  }
30
-
35
+
36
+
37
+ static VALUE
38
+ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
+ {
40
+ VALUE arities_key = rb_intern(ARITIES_KEY);
41
+ VALUE arities = rb_ivar_get(mod, arities_key);
42
+
43
+ if (arities == Qundef || NIL_P(arities)) {
44
+ arities = rb_hash_new();
45
+ rb_ivar_set(mod, arities_key, arities);
46
+ }
47
+ rb_hash_aset(arities, name, arity);
48
+
49
+ return rb_funcall(mod, rb_intern("define_method"), 2, name, proc);
50
+ }
51
+
52
+ static VALUE
53
+ am_get_method_arity(VALUE method, VALUE orig_arity_method)
54
+ {
55
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
56
+ VALUE arities = rb_ivar_get(owner, rb_intern(ARITIES_KEY));
57
+ VALUE name = rb_funcall(method, rb_intern("name"), 0);
58
+ VALUE arity = Qnil;
59
+ // See if we saved an arity for the method.
60
+ if (!NIL_P(arities)) {
61
+ arity = rb_hash_aref(arities, name);
62
+ }
63
+ // Didn't find one, call the original method.
64
+ if (NIL_P(arity)) {
65
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
+ }
68
+
69
+ return arity;
70
+ }
71
+
72
+ static VALUE
73
+ am_unbound_method_arity(VALUE method)
74
+ {
75
+ VALUE orig_unbound_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@unbound_method_arity"));
76
+ return am_get_method_arity(method, orig_unbound_method_arity);
77
+ }
78
+
79
+ static VALUE
80
+ am_method_arity(VALUE method)
81
+ {
82
+ VALUE orig_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@method_arity"));
83
+ return am_get_method_arity(method, orig_method_arity);
84
+ }
85
+
31
86
  void Init_appmap() {
32
87
  VALUE appmap = rb_define_module("AppMap");
33
- VALUE hook = rb_define_class_under(appmap, "Hook", rb_cObject);
88
+ am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
+
90
+ rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
34
91
 
35
- rb_define_singleton_method(hook, "singleton_method_owner_name", singleton_method_owner_name, 1);
92
+ rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
+ rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
94
+ rb_define_method(rb_cMethod, "arity", am_method_arity, 0);
36
95
  }
@@ -84,7 +84,7 @@ module AppMap
84
84
 
85
85
  # Builds a class map from a config and a list of Ruby methods.
86
86
  def class_map(methods)
87
- ClassMap.build_from_methods(configuration, methods)
87
+ ClassMap.build_from_methods(methods)
88
88
  end
89
89
 
90
90
  # Returns default metadata detected from the Ruby system and from the
@@ -61,30 +61,29 @@ module AppMap
61
61
  location: location,
62
62
  static: static,
63
63
  labels: labels
64
- }.delete_if { |k,v| v.nil? || v == [] }
64
+ }.delete_if { |_, v| v.nil? || v == [] }
65
65
  end
66
66
  end
67
67
  end
68
68
 
69
69
  class << self
70
- def build_from_methods(config, methods)
70
+ def build_from_methods(methods)
71
71
  root = Types::Root.new
72
72
  methods.each do |method|
73
- package = config.package_for_method(method) \
74
- or raise "No package found for method #{method}"
75
- add_function root, package, method
73
+ add_function root, method
76
74
  end
77
75
  root.children.map(&:to_h)
78
76
  end
79
77
 
80
78
  protected
81
79
 
82
- def add_function(root, package, method)
80
+ def add_function(root, method)
81
+ package = method.package
83
82
  static = method.static
84
83
 
85
84
  object_infos = [
86
85
  {
87
- name: package.path,
86
+ name: package.name,
88
87
  type: 'package'
89
88
  }
90
89
  ]
@@ -2,15 +2,39 @@
2
2
 
3
3
  module AppMap
4
4
  class Config
5
- Package = Struct.new(:path, :package_name, :exclude, :labels) do
6
- def initialize(path, package_name: nil, exclude: [], labels: [])
7
- super path, package_name, exclude, labels
5
+ Package = Struct.new(:path, :gem, :package_name, :exclude, :labels) do
6
+ class << self
7
+ def build_from_path(path, package_name: nil, exclude: [], labels: [])
8
+ Package.new(path, nil, package_name, exclude, labels)
9
+ end
10
+
11
+ def build_from_gem(gem, package_name: nil, exclude: [], labels: [])
12
+ gem_paths(gem).map do |gem_path|
13
+ Package.new(gem_path, gem, package_name, exclude, labels)
14
+ end
15
+ end
16
+
17
+ private_class_method :new
18
+
19
+ protected
20
+
21
+ def gem_paths(gem)
22
+ gemspec = Gem.loaded_specs[gem] or raise "Gem #{gem.inspect} not found"
23
+ gemspec.source_paths.map do |path|
24
+ File.join(gemspec.gem_dir, path)
25
+ end
26
+ end
27
+ end
28
+
29
+ def name
30
+ gem || path
8
31
  end
9
-
32
+
10
33
  def to_h
11
34
  {
12
35
  path: path,
13
36
  package_name: package_name,
37
+ gem: gem,
14
38
  exclude: exclude.blank? ? nil : exclude,
15
39
  labels: labels.blank? ? nil : labels
16
40
  }.compact
@@ -20,31 +44,29 @@ module AppMap
20
44
  Hook = Struct.new(:method_names, :package) do
21
45
  end
22
46
 
23
- OPENSSL_PACKAGE = Package.new('openssl', package_name: 'openssl', labels: %w[security crypto])
47
+ OPENSSL_PACKAGES = Package.build_from_path('openssl', package_name: 'openssl', labels: %w[security crypto])
24
48
 
25
49
  # Methods that should always be hooked, with their containing
26
50
  # package and labels that should be applied to them.
27
51
  HOOKED_METHODS = {
28
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.new('active_support', package_name: 'active_support', labels: %w[security crypto]))
52
+ 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[security crypto]))
29
53
  }.freeze
30
54
 
31
55
  BUILTIN_METHODS = {
32
- 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGE),
33
- 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGE),
34
- 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGE),
35
- 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGE),
36
- 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGE),
37
- 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGE),
38
- 'Logger' => Hook.new(:add, Package.new('logger', labels: %w[log io])),
39
- 'Net::HTTP' => Hook.new(:request, Package.new('net/http', package_name: 'net/http', labels: %w[http io])),
40
- 'Net::SMTP' => Hook.new(:send, Package.new('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
41
- 'Net::POP3' => Hook.new(:mails, Package.new('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
42
- 'Net::IMAP' => Hook.new(:send_command, Package.new('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
43
- 'IO' => Hook.new(%i[read write open close], Package.new('io', labels: %w[io])),
44
- 'Marshal' => Hook.new(%i[dump load], Package.new('marshal', labels: %w[serialization marshal])),
45
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.new('yaml', package_name: 'psych', labels: %w[serialization yaml])),
46
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.new('json', package_name: 'json', labels: %w[serialization json])),
47
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.new('json', package_name: 'json', labels: %w[serialization json]))
56
+ 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
57
+ 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGES),
58
+ 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
59
+ 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
60
+ 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
61
+ 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
62
+ 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[http io])),
63
+ 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
64
+ 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
65
+ 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
66
+ 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[serialization marshal])),
67
+ 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[serialization yaml])),
68
+ 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json])),
69
+ 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json]))
48
70
  }.freeze
49
71
 
50
72
  attr_reader :name, :packages
@@ -64,8 +86,16 @@ module AppMap
64
86
  # Loads configuration from a Hash.
65
87
  def load(config_data)
66
88
  packages = (config_data['packages'] || []).map do |package|
67
- Package.new(package['path'], exclude: package['exclude'] || [])
68
- end
89
+ gem = package['gem']
90
+ path = package['path']
91
+ raise 'AppMap package configuration should specify gem or path, not both' if gem && path
92
+
93
+ if gem
94
+ Package.build_from_gem(gem, exclude: package['exclude'] || [])
95
+ else
96
+ [ Package.build_from_path(path, exclude: package['exclude'] || []) ]
97
+ end
98
+ end.flatten
69
99
  Config.new config_data['name'], packages
70
100
  end
71
101
  end
@@ -105,7 +135,7 @@ module AppMap
105
135
  hook = find_hook(defined_class)
106
136
  return nil unless hook
107
137
 
108
- Array(hook.method_names).include?(method_name) ? hook.package : nil
138
+ Array(hook.method_names).include?(method_name) ? hook.package : nil
109
139
  end
110
140
 
111
141
  def find_hook(defined_class)
@@ -31,25 +31,43 @@ module AppMap
31
31
  def display_string(value)
32
32
  return nil unless value
33
33
 
34
+ value_string = custom_display_string(value) || default_display_string(value)
35
+
36
+ (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
+ end
38
+
39
+ protected
40
+
41
+ def custom_display_string(value)
42
+ case value
43
+ when File
44
+ "#{value.class}[path=#{value.path}]"
45
+ when Net::HTTP
46
+ "#{value.class}[#{value.address}:#{value.port}]"
47
+ when Net::HTTPGenericRequest
48
+ "#{value.class}[#{value.method} #{value.path}]"
49
+ end
50
+ rescue StandardError
51
+ nil
52
+ end
53
+
54
+ def default_display_string(value)
34
55
  last_resort_string = lambda do
35
56
  warn "AppMap encountered an error inspecting a #{value.class.name}: #{$!.message}"
36
57
  '*Error inspecting variable*'
37
58
  end
38
59
 
39
- value_string = \
60
+ begin
61
+ value.to_s
62
+ rescue NoMethodError
40
63
  begin
41
- value.to_s
42
- rescue NoMethodError
43
- begin
44
- value.inspect
45
- rescue StandardError
46
- last_resort_string.call
47
- end
64
+ value.inspect
48
65
  rescue StandardError
49
66
  last_resort_string.call
50
67
  end
51
-
52
- (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
68
+ rescue StandardError
69
+ last_resort_string.call
70
+ end
53
71
  end
54
72
  end
55
73
  end
@@ -4,7 +4,10 @@ require 'English'
4
4
 
5
5
  module AppMap
6
6
  class Hook
7
- LOG = (ENV['DEBUG'] == 'true')
7
+ LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
+
9
+ @unbound_method_arity = ::UnboundMethod.instance_method(:arity)
10
+ @method_arity = ::Method.instance_method(:arity)
8
11
 
9
12
  class << self
10
13
  def lock_builtins
@@ -45,7 +48,6 @@ module AppMap
45
48
  hook = lambda do |hook_cls|
46
49
  lambda do |method_id|
47
50
  method = hook_cls.public_instance_method(method_id)
48
- hook_method = Hook::Method.new(hook_cls, method)
49
51
 
50
52
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
51
53
 
@@ -53,14 +55,16 @@ module AppMap
53
55
  # Skip methods that have no instruction sequence, as they are obviously trivial.
54
56
  next unless disasm
55
57
 
56
- # Don't try and trace the AppMap methods or there will be
57
- # a stack overflow in the defined hook method.
58
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
59
-
60
58
  next unless \
61
59
  config.always_hook?(hook_cls, method.name) ||
62
60
  config.included_by_location?(method)
63
61
 
62
+ hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
63
+
64
+ # Don't try and trace the AppMap methods or there will be
65
+ # a stack overflow in the defined hook method.
66
+ next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
67
+
64
68
  hook_method.activate
65
69
  end
66
70
  end
@@ -94,9 +98,9 @@ module AppMap
94
98
  end
95
99
 
96
100
  if method
97
- Hook::Method.new(cls, method).activate
101
+ Hook::Method.new(hook.package, cls, method).activate
98
102
  else
99
- warn "Method #{method_name} not found on #{cls.name}"
103
+ warn "Method #{method_name} not found on #{cls.name}"
100
104
  end
101
105
  end
102
106
  end