appmap 0.42.0 → 0.45.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.releaserc.yml +11 -0
  3. data/.travis.yml +23 -2
  4. data/CHANGELOG.md +42 -0
  5. data/README.md +65 -6
  6. data/README_CI.md +29 -0
  7. data/Rakefile +4 -2
  8. data/appmap.gemspec +5 -3
  9. data/lib/appmap.rb +4 -7
  10. data/lib/appmap/class_map.rb +7 -10
  11. data/lib/appmap/command/record.rb +1 -1
  12. data/lib/appmap/config.rb +173 -67
  13. data/lib/appmap/cucumber.rb +1 -1
  14. data/lib/appmap/event.rb +18 -0
  15. data/lib/appmap/handler/function.rb +19 -0
  16. data/lib/appmap/handler/net_http.rb +107 -0
  17. data/lib/appmap/hook.rb +112 -56
  18. data/lib/appmap/hook/method.rb +5 -7
  19. data/lib/appmap/middleware/remote_recording.rb +1 -1
  20. data/lib/appmap/minitest.rb +22 -20
  21. data/lib/appmap/rails/request_handler.rb +30 -17
  22. data/lib/appmap/record.rb +1 -1
  23. data/lib/appmap/rspec.rb +23 -21
  24. data/lib/appmap/trace.rb +2 -1
  25. data/lib/appmap/util.rb +47 -2
  26. data/lib/appmap/version.rb +2 -2
  27. data/release.sh +17 -0
  28. data/spec/abstract_controller_base_spec.rb +77 -30
  29. data/spec/class_map_spec.rb +3 -11
  30. data/spec/config_spec.rb +33 -1
  31. data/spec/fixtures/hook/custom_instance_method.rb +11 -0
  32. data/spec/fixtures/hook/method_named_call.rb +11 -0
  33. data/spec/fixtures/rails5_users_app/Gemfile +7 -3
  34. data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
  35. data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
  36. data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
  37. data/spec/fixtures/rails5_users_app/create_app +8 -2
  38. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  39. data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
  40. data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
  41. data/spec/fixtures/rails6_users_app/Gemfile +5 -4
  42. data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
  43. data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
  44. data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
  45. data/spec/fixtures/rails6_users_app/create_app +8 -2
  46. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
  47. data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
  48. data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
  49. data/spec/hook_spec.rb +141 -20
  50. data/spec/record_net_http_spec.rb +160 -0
  51. data/spec/record_sql_rails_pg_spec.rb +1 -1
  52. data/spec/spec_helper.rb +16 -0
  53. data/test/expectations/openssl_test_key_sign1.json +2 -4
  54. data/test/gem_test.rb +1 -1
  55. data/test/rspec_test.rb +0 -13
  56. metadata +17 -12
  57. data/exe/appmap +0 -154
  58. data/test/cli_test.rb +0 -116
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50826fcf91733d3bb29aad0b6c77af2516ad08aa87557c3675d4be896f7790db
4
- data.tar.gz: 4e5ee3f431b68a69de5ff6b080eeae861f3f7de6fe952ea1e71b5c30d4777bc1
3
+ metadata.gz: b91b79723565f45d9d59ce341a9944a3a4e1c853bc77c1b176dc7b26ede9df22
4
+ data.tar.gz: f9be88ae7e83b801f66ada4087a06923ad9fa4d10e498f1c8b07fe0e301552d7
5
5
  SHA512:
6
- metadata.gz: fcd01c7d1a3d70c29e33d8890e8f1ea8d7dc5cd2fbca7724618fcc76190c2d0217b89ba69e4de5003eb80147d44c15335cfdff60e67cdaf3eeaa18655fdf2b4f
7
- data.tar.gz: 32c714968c8669f3a9dceb20543a0f09856948f4b4d6b8584ad06d6347cf10f28a4d0386e4f3af1aaccb1709ec2a88fefe88da88104ffbe18dfd0fd81fb5cb95
6
+ metadata.gz: e7eda447c67a44ad10226f5b6e9eb51c82995a4b2df970af27b0e2b5304101d419a6b3843ef6f0a983a3f952da2e3ed55f0de327b2856fcf970961c410c19b79
7
+ data.tar.gz: 839df6608d503e74f663d42673d9cf91866592a1715cee1c828c3de23ae472fdff6025ea9ee767a33e384b2ed51f46a999001d354f3e763deba46a5253eb5661
data/.releaserc.yml ADDED
@@ -0,0 +1,11 @@
1
+ plugins:
2
+ - '@semantic-release/commit-analyzer'
3
+ - '@semantic-release/release-notes-generator'
4
+ - '@semantic-release/changelog'
5
+ - 'semantic-release-rubygem'
6
+ - - '@semantic-release/git'
7
+ - assets:
8
+ - CHANGELOG.md
9
+ - appmap.gemspec
10
+ - lib/appmap/version.rb
11
+ - '@semantic-release/github'
data/.travis.yml CHANGED
@@ -13,11 +13,32 @@ services:
13
13
  # necessary.
14
14
  before_script:
15
15
  - unset RAILS_ENV
16
-
16
+
17
+ cache:
18
+ bundler: true
19
+
20
+
21
+ # GEM_ALTERNATIVE_NAME only needed for deployment
17
22
  jobs:
18
23
  include:
19
24
  - stage: test
20
25
  script:
21
26
  - mkdir tmp
22
- - bundle exec rake test
27
+ - GEM_ALTERNATIVE_NAME='' bundle exec rake test
28
+
23
29
 
30
+ before_deploy:
31
+ - |
32
+ nvm install --lts \
33
+ && nvm use --lts \
34
+ && npm i -g \
35
+ semantic-release \
36
+ @semantic-release/git \
37
+ @semantic-release/changelog \
38
+ semantic-release-rubygem
39
+
40
+ deploy:
41
+ - provider: script
42
+ script: ./release.sh
43
+ on:
44
+ branch: master
data/CHANGELOG.md CHANGED
@@ -1,3 +1,45 @@
1
+ ## [0.45.1](https://github.com/applandinc/appmap-ruby/compare/v0.45.0...v0.45.1) (2021-05-04)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Optimize instrumentation and load time ([db4a8ce](https://github.com/applandinc/appmap-ruby/commit/db4a8ceed4103a52caafa46626c66f33fbfeac27))
7
+
8
+ # [0.45.0](https://github.com/applandinc/appmap-ruby/compare/v0.44.0...v0.45.0) (2021-05-03)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Properly name status_code in HTTP server response ([556e87c](https://github.com/applandinc/appmap-ruby/commit/556e87c9a7bf214f6b8714add4f77448fd223d33))
14
+
15
+
16
+ ### Features
17
+
18
+ * Record http_client_request and http_client_response ([1db32ae](https://github.com/applandinc/appmap-ruby/commit/1db32ae0d26a7f1400b6b814d25b13368f06c158))
19
+ * Update AppMap format version to 1.5.0 ([061705e](https://github.com/applandinc/appmap-ruby/commit/061705e4619cb881e8edd022ef835183e399e127))
20
+ * **build:** add deployment via `semantic-release` with automatic publication to rubygems ([9f183de](https://github.com/applandinc/appmap-ruby/commit/9f183de13f405900000c3da979c3a8a5b6e34a24))
21
+
22
+ # v0.44.0
23
+
24
+ * Support recording and labeling of indivudal functions via `functions:` section in *appmap.yml*.
25
+ * Remove deprecated `exe/appmap`.
26
+ * Add `test_status` and `exception` fields to AppMap metadata.
27
+ * Write AppMap file atomically, by writing to a temp file first and then moving it into place.
28
+ * Remove printing of `Inventory.json` file.
29
+ * Remove source code from `classMap`.
30
+
31
+ # v0.43.0
32
+
33
+ * Record `name` and `class` of each entry in Hash-like parameters, messages, and return values.
34
+ * Record client-sent headers in HTTP server request and response.
35
+ * Record HTTP server request `mime_type`.
36
+ * Record HTTP server request `authorization`.
37
+
38
+ # v0.42.1
39
+
40
+ * Add missing require `set`.
41
+ * Check `cls.respond_to?(:singleton_class)`, since it oddly, may not.
42
+
1
43
  # v0.42.0
2
44
 
3
45
  * Remove `feature_group` and `feature` metadata from minitest and RSpec AppMaps.
data/README.md CHANGED
@@ -11,8 +11,10 @@
11
11
  - [Remote recording](#remote-recording)
12
12
  - [Server process recording](#server-process-recording)
13
13
  - [AppMap for VSCode](#appmap-for-vscode)
14
+ - [AppMap Swagger](#appmap-swagger)
14
15
  - [Uploading AppMaps](#uploading-appmaps)
15
16
  - [Development](#development)
17
+ - [Internal architecture](#internal-architecture)
16
18
  - [Running tests](#running-tests)
17
19
  - [Using fixture apps](#using-fixture-apps)
18
20
  - [`test/fixtures`](#testfixtures)
@@ -109,6 +111,9 @@ name: my_project
109
111
  packages:
110
112
  - path: app/controllers
111
113
  - path: app/models
114
+ # Exclude sub-paths within the package path
115
+ exclude:
116
+ - concerns/accessor
112
117
  - path: app/jobs
113
118
  - path: app/helpers
114
119
  # Include the gems that you want to see in the dependency maps.
@@ -117,15 +122,22 @@ packages:
117
122
  - gem: devise
118
123
  - gem: aws-sdk
119
124
  - gem: will_paginate
125
+ # Global exclusion of a class name
120
126
  exclude:
121
127
  - MyClass
122
128
  - MyClass#my_instance_method
123
129
  - MyClass.my_class_method
130
+ functions:
131
+ - packages: myapp
132
+ class: ControllerHelper
133
+ function: logged_in_user
134
+ labels: [ authentication ]
124
135
  ```
125
136
 
126
137
  * **name** Provides the project name (required)
127
138
  * **packages** A list of source code directories which should be recorded.
128
139
  * **exclude** A list of classes and/or methods to definitively exclude from recording.
140
+ * **functions** A list of specific functions, scoped by package and class, to record.
129
141
 
130
142
  **packages**
131
143
 
@@ -144,6 +156,11 @@ Each entry in the `packages` list is a YAML object which has the following keys:
144
156
 
145
157
  Optional list of fully qualified class and method names. Separate class and method names with period (`.`) for class methods and hash (`#`) for instance methods.
146
158
 
159
+ **functions**
160
+
161
+ Optional list of `class, function` pairs. The `package` name is used to place the function within the class map, and does not have to match
162
+ the folder or gem name. The primary use of `functions` is to apply specific labels to functions whose source code is not accessible (e.g., it's in a Gem).
163
+ For functions which are part of the application code, use `@label` or `@labels` in code comments to apply labels.
147
164
 
148
165
  # Labels
149
166
 
@@ -329,9 +346,20 @@ Run your Rails server with `APPMAP_RECORD=true`. When the server exits, an *appm
329
346
 
330
347
  Be sure and set `WEB_CONCURRENCY=1`, if you are using a webserver that can run multiple processes. You only want there to be one process while you are recording, otherwise they will both try and write *appmap.json* and one of them will clobber the other.
331
348
 
349
+
332
350
  # AppMap for VSCode
333
351
 
334
- The [AppMap extension for VSCode](https://marketplace.visualstudio.com/items?itemName=appland.appmap) is a great way to onboard developers to new code, and troubleshoot hard-to-understand bugs with visuals.
352
+ The [AppMap extension for VSCode](https://marketplace.visualstudio.com/items?itemName=appland.appmap) helps you navigate your code more efficiently with interactive, accurate software architecture diagrams right in your IDE. In less than two minutes you can go from installing the AppMap extension to exploring maps of your code's architecture. AppMap helps you:
353
+
354
+ * Onboard to code architecture, with no extra work for the team
355
+ * Conduct code and design reviews using live and accurate data
356
+ * Troubleshoot hard-to-understand bugs using a "top-down" approach.
357
+
358
+ Each interactive diagram links directly to the source code, and the information is easy to share.
359
+
360
+ # AppMap Swagger
361
+
362
+ [appmap_swagger](https://github.com/applandinc/appmap_swagger-ruby) is a tool to generate Swagger files from AppMap data. With `appmap_swagger`, you can add Swagger to your Ruby or Ruby on Rails project, with no need to write or modify code. Use the Swagger UI to interact with your web services API as you build it, and use diffs of Swagger to perform code review of web service changes.
335
363
 
336
364
  # Uploading AppMaps
337
365
 
@@ -342,6 +370,34 @@ For instructions on uploading, see the documentation of the [AppLand CLI](https:
342
370
  # Development
343
371
  [![Build Status](https://travis-ci.com/applandinc/appmap-ruby.svg?branch=master)](https://travis-ci.com/applandinc/appmap-ruby)
344
372
 
373
+ ## Internal architecture
374
+
375
+ **Configuration**
376
+
377
+ *appmap.yml* is loaded into an `AppMap::Config`.
378
+
379
+ **Hooking**
380
+
381
+ Once configuration is loaded, `AppMap::Hook` is enabled. "Hooking" refers to the process of replacing a method
382
+ with a "hooked" version of the method. The hooked method checks to see if tracing is enabled. If so, it wraps the original
383
+ method with calls that record the parameters and return value.
384
+
385
+ **Builtins**
386
+
387
+ `Hook` begins by iterating over builtin classes and modules defined in the `Config`. Builtins include code
388
+ like `openssl` and `net/http`. This code is not dependent on any external libraries being present, and
389
+ `appmap` cannot guarantee that it will be loaded before builtins. Therefore, it's necessary to require it and
390
+ hook it by looking up the classes and modules as constants in the `Object` namespace.
391
+
392
+ **User code and gems**
393
+
394
+ After hooking builtins, `Hook` attaches a [TracePoint](https://ruby-doc.org/core-2.6/TracePoint.html) to `:begin` events.
395
+ This TracePoint is notified each time a new class or module is being evaluated. When this happens, `Hook` uses the `Config`
396
+ to determine whether any code within the evaluated file is configured for hooking. If so, a `TracePoint` is attached to
397
+ `:end` events. Each `:end` event is fired when a class or module definition is completed. When this happens, the `Hook` enumerates
398
+ the public methods of the class or module, hooking the ones that are targeted by the `Config`. Once the `:end` TracePoint leaves
399
+ the scope of the `:begin`, the `:end` TracePoint is disabled.
400
+
345
401
  ## Running tests
346
402
 
347
403
  Before running tests, configure `local.appmap` to point to your local `appmap-ruby` directory.
@@ -369,7 +425,7 @@ The fixture apps in `test/fixtures` are plain Ruby projects that exercise the ba
369
425
 
370
426
  ### `spec/fixtures`
371
427
 
372
- The fixture apps in `spec/fixtures` are simple Rack, Rails4, and Rails5 apps.
428
+ The fixture apps in `spec/fixtures` are simple Rack, Rails5, and Rails6 apps.
373
429
  You can use them to interactively develop and test the recording features of the `appmap` gem.
374
430
  These fixture apps are more sophisticated than `test/fixtures`, because they include additional
375
431
  resources such as a PostgreSQL database.
@@ -395,11 +451,15 @@ $ docker-compose run --rm app ./create_app
395
451
  Now you can start a development container.
396
452
 
397
453
  ```sh-session
398
- $ docker-compose run --rm -v $PWD/../../..:/src/appmap-ruby app bash
454
+ $ docker-compose run --rm -v $PWD:/app -v $PWD/../../..:/src/appmap-ruby app bash
399
455
  Starting rails_users_app_pg_1 ... done
400
- root@6fab5f89125f:/app# cd /src/app
456
+ root@6fab5f89125f:/app# cd /src/appmap-ruby
457
+ root@6fab5f89125f:/src/appmap-ruby# rm ext/appmap/*.so ext/appmap/*.o
458
+ root@6fab5f89125f:/src/appmap-ruby# bundle
459
+ root@6fab5f89125f:/src/appmap-ruby# bundle exec rake compile
460
+ root@6fab5f89125f:/src/appmap-ruby# cd /src/app
401
461
  root@6fab5f89125f:/src/app# bundle config local.appmap /src/appmap-ruby
402
- root@6fab5f89125f:/src/app# bundle update appmap
462
+ root@6fab5f89125f:/src/app# bundle
403
463
  ```
404
464
 
405
465
  At this point, the bundle is built with the `appmap` gem located in `/src/appmap`, which is volume-mounted from the host.
@@ -414,4 +474,3 @@ Configuring AppMap from path appmap.yml
414
474
  Finished in 0.07357 seconds (files took 2.1 seconds to load)
415
475
  4 examples, 0 failures
416
476
  ```
417
-
data/README_CI.md ADDED
@@ -0,0 +1,29 @@
1
+ # Configuration variables:
2
+
3
+ * `GH_TOKEN`: used by `semantic-release` to push changes to Github and manage releases
4
+ * `GEM_HOST_API_KEY`: rubygems API key
5
+ * `GEM_ALTERNATIVE_NAME` (optional): used for testing of CI flows,
6
+ to avoid publication of test releases under official package name
7
+ * `DOCKERHUB\_USERNAME`, `DOCKERHUB_PASSWORD`: optional dockerhub credentials,
8
+ to avoid throttling of dockerhub anonymous pulls
9
+
10
+ Note: for security reasons, it's better to use dedicated (not personal)
11
+ Dockerhub account,
12
+ and also use [access tokens](https://docs.docker.com/docker-hub/access-tokens/)
13
+ instead of primary password
14
+
15
+ # Release command
16
+
17
+ `./release.sh`
18
+
19
+ Bash wrapper script is used merely as a launcher of `semantic-release`
20
+ with extra logic to explicitly determine git url from `TRAVIS_REPO_SLUG` \
21
+ variable if its defined (otherwise git url is taken from `package.json`,
22
+ which breaks CI on forked repos).
23
+
24
+ # CI flow
25
+
26
+ 1. Test happens using current version number specified in `lib/appmap/version.rb`, then `release.sh` launches `semantic-release` to do the rest
27
+ 2. The version number is increased (including modicication of `version.rb`)
28
+ 3. Gem is published under new version number
29
+ 4. Github release is created with the new version number
data/Rakefile CHANGED
@@ -37,7 +37,8 @@ end
37
37
 
38
38
  def build_base_image(ruby_version)
39
39
  run_cmd "docker build" \
40
- " --build-arg RUBY_VERSION=#{ruby_version} --build-arg GEM_VERSION=#{GEM_VERSION}" \
40
+ " --build-arg RUBY_VERSION=#{ruby_version}" \
41
+ " --build-arg GEM_VERSION=#{GEM_VERSION}" \
41
42
  " -t appmap:#{GEM_VERSION} -f Dockerfile.appmap ."
42
43
  end
43
44
 
@@ -46,7 +47,7 @@ def build_app_image(app, ruby_version)
46
47
  run_cmd( {"RUBY_VERSION" => ruby_version, "GEM_VERSION" => GEM_VERSION},
47
48
  " docker-compose build" \
48
49
  " --build-arg RUBY_VERSION=#{ruby_version}" \
49
- " --build-arg GEM_VERSION=#{GEM_VERSION}")
50
+ " --build-arg GEM_VERSION=#{GEM_VERSION}" )
50
51
  end
51
52
  end
52
53
 
@@ -138,3 +139,4 @@ task spec: %i[spec:all]
138
139
  task test: %i[spec:all minitest]
139
140
 
140
141
  task default: :test
142
+
data/appmap.gemspec CHANGED
@@ -4,8 +4,12 @@ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'appmap/version'
6
6
 
7
+
8
+
7
9
  Gem::Specification.new do |spec|
8
- spec.name = 'appmap'
10
+ # ability to parameterize gem name is added intentionally,
11
+ # to support the possibility of unofficial releases, e.g. during CI tests
12
+ spec.name = (ENV['GEM_ALTERNATIVE_NAME'].to_s.empty? ? 'appmap' : ENV["GEM_ALTERNATIVE_NAME"] )
9
13
  spec.version = AppMap::VERSION
10
14
  spec.authors = ['Kevin Gilpin']
11
15
  spec.email = ['kgilpin@gmail.com']
@@ -20,8 +24,6 @@ Gem::Specification.new do |spec|
20
24
  ")
21
25
  spec.extensions << "ext/appmap/extconf.rb"
22
26
 
23
- spec.bindir = 'exe'
24
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
27
  spec.require_paths = ['lib']
26
28
 
27
29
  spec.add_dependency 'activesupport'
data/lib/appmap.rb CHANGED
@@ -9,6 +9,7 @@ end
9
9
 
10
10
  require 'appmap/version'
11
11
  require 'appmap/hook'
12
+ require 'appmap/handler/net_http'
12
13
  require 'appmap/config'
13
14
  require 'appmap/trace'
14
15
  require 'appmap/class_map'
@@ -43,6 +44,7 @@ module AppMap
43
44
  # Call this function before the program code is loaded by the Ruby VM, otherwise
44
45
  # the load events won't be seen and the hooks won't activate.
45
46
  def initialize(config_file_path = 'appmap.yml')
47
+ raise "AppMap configuration file #{config_file_path} does not exist" unless ::File.exists?(config_file_path)
46
48
  warn "Configuring AppMap from path #{config_file_path}"
47
49
  Config.load_from_file(config_file_path).tap do |configuration|
48
50
  self.configuration = configuration
@@ -50,11 +52,6 @@ module AppMap
50
52
  end
51
53
  end
52
54
 
53
- # Whether to include source and comments in all class maps.
54
- def include_source?
55
- ENV['APPMAP_SOURCE'] == 'true'
56
- end
57
-
58
55
  # Used to start tracing, stop tracing, and record events.
59
56
  def tracing
60
57
  @tracing ||= Trace::Tracing.new
@@ -88,8 +85,8 @@ module AppMap
88
85
  end
89
86
 
90
87
  # Builds a class map from a config and a list of Ruby methods.
91
- def class_map(methods, options = {})
92
- ClassMap.build_from_methods(methods, options)
88
+ def class_map(methods)
89
+ ClassMap.build_from_methods(methods)
93
90
  end
94
91
 
95
92
  # Returns default metadata detected from the Ruby system and from the
@@ -71,17 +71,17 @@ module AppMap
71
71
  end
72
72
 
73
73
  class << self
74
- def build_from_methods(methods, options = {})
74
+ def build_from_methods(methods)
75
75
  root = Types::Root.new
76
76
  methods.each do |method|
77
- add_function root, method, options
77
+ add_function root, method
78
78
  end
79
79
  root.children.map(&:to_h)
80
80
  end
81
81
 
82
82
  protected
83
83
 
84
- def add_function(root, method, include_source: true)
84
+ def add_function(root, method)
85
85
  package = method.package
86
86
  static = method.static
87
87
 
@@ -113,16 +113,13 @@ module AppMap
113
113
  [ method.defined_class, static ? '.' : '#', method.name ].join
114
114
  end
115
115
 
116
- source, comment = begin
117
- [ method.source, method.comment ]
116
+ comment = begin
117
+ method.comment
118
118
  rescue MethodSource::SourceNotFoundError
119
- [ nil, nil, ]
119
+ nil
120
120
  end
121
121
 
122
- if include_source
123
- function_info[:source] = source unless source.blank?
124
- function_info[:comment] = comment unless comment.blank?
125
- end
122
+ function_info[:comment] = comment unless comment.blank?
126
123
 
127
124
  function_info[:labels] = parse_labels(comment) + (package.labels || [])
128
125
  object_infos << function_info
@@ -27,7 +27,7 @@ module AppMap
27
27
  event_thread.join
28
28
  yield AppMap::APPMAP_FORMAT_VERSION,
29
29
  AppMap.detect_metadata,
30
- AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
30
+ AppMap.class_map(tracer.event_methods),
31
31
  events
32
32
  end
33
33
 
data/lib/appmap/config.rb CHANGED
@@ -2,7 +2,28 @@
2
2
 
3
3
  module AppMap
4
4
  class Config
5
+ # Specifies a code +path+ to be mapped.
6
+ # Options:
7
+ #
8
+ # * +gem+ may indicate a gem name that "owns" the path
9
+ # * +package_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
10
+ # builtins, or when the path to be required is not automatically required when bundler requires the gem.
11
+ # * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
12
+ # * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
13
+ # specific functions, via TargetMethods.
14
+ # * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
5
15
  Package = Struct.new(:path, :gem, :package_name, :exclude, :labels, :shallow) do
16
+ # This is for internal use only.
17
+ private_methods :gem
18
+
19
+ # Specifies the class that will convert code events into event objects.
20
+ attr_writer :handler_class
21
+
22
+ def handler_class
23
+ require 'appmap/handler/function'
24
+ @handler_class || AppMap::Handler::Function
25
+ end
26
+
6
27
  # Indicates that only the entry points to a package will be recorded.
7
28
  # Once the code has entered a package, subsequent calls within the package will not be
8
29
  # recorded unless the code leaves the package and re-enters it.
@@ -11,25 +32,36 @@ module AppMap
11
32
  end
12
33
 
13
34
  class << self
35
+ # Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
36
+ # in appmap.yml. Also used for mapping specific methods via TargetMethods.
14
37
  def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
15
38
  Package.new(path, nil, package_name, exclude, labels, shallow)
16
39
  end
17
40
 
18
- def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
19
- if %w[method_source activesupport].member?(gem)
41
+ # Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
42
+ # a builtin.
43
+ def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [], optional: false, force: false)
44
+ if !force && %w[method_source activesupport].member?(gem)
20
45
  warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
21
46
  return
22
47
  end
23
- Package.new(gem_path(gem), gem, package_name, exclude, labels, shallow)
48
+ path = gem_path(gem, optional)
49
+ if path
50
+ Package.new(path, gem, package_name, exclude, labels, shallow)
51
+ else
52
+ warn "#{gem} is not available in the bundle" if AppMap::Hook::LOG
53
+ end
24
54
  end
25
55
 
26
56
  private_class_method :new
27
57
 
28
58
  protected
29
59
 
30
- def gem_path(gem)
31
- gemspec = Gem.loaded_specs[gem] or raise "Gem #{gem.inspect} not found"
32
- gemspec.gem_dir
60
+ def gem_path(gem, optional)
61
+ gemspec = Gem.loaded_specs[gem]
62
+ # This exception will notify a user that their appmap.yml contains non-existent gems.
63
+ raise "Gem #{gem.inspect} not found" unless gemspec || optional
64
+ gemspec ? gemspec.gem_dir : nil
33
65
  end
34
66
  end
35
67
 
@@ -42,6 +74,7 @@ module AppMap
42
74
  path: path,
43
75
  package_name: package_name,
44
76
  gem: gem,
77
+ handler_class: handler_class.name,
45
78
  exclude: exclude.blank? ? nil : exclude,
46
79
  labels: labels.blank? ? nil : labels,
47
80
  shallow: shallow
@@ -49,44 +82,105 @@ module AppMap
49
82
  end
50
83
  end
51
84
 
52
- Hook = Struct.new(:method_names, :package) do
85
+ Function = Struct.new(:package, :cls, :labels, :function_names) do # :nodoc:
86
+ def to_h
87
+ {
88
+ package: package,
89
+ class: cls,
90
+ labels: labels,
91
+ functions: function_names.map(&:to_sym)
92
+ }.compact
93
+ end
94
+ end
95
+ private_constant :Function
96
+
97
+ class TargetMethods # :nodoc:
98
+ attr_reader :method_names, :package
99
+
100
+ def initialize(method_names, package)
101
+ @method_names = method_names
102
+ @package = package
103
+ end
104
+
105
+ def include_method?(method_name)
106
+ Array(method_names).include?(method_name)
107
+ end
108
+
109
+ def to_h
110
+ {
111
+ package: package.name,
112
+ method_names: method_names
113
+ }
114
+ end
53
115
  end
116
+ private_constant :TargetMethods
54
117
 
55
- OPENSSL_PACKAGES = Package.build_from_path('openssl', package_name: 'openssl', labels: %w[security crypto])
118
+ OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
56
119
 
57
120
  # Methods that should always be hooked, with their containing
58
121
  # package and labels that should be applied to them.
59
122
  HOOKED_METHODS = {
60
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[provider.secure_compare])),
61
- 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', labels: %w[mvc.view])),
62
- 'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[provider.http.cookie])),
63
- 'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[provider.http.cookie crypto])),
64
- 'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[provider.authorization])),
65
- 'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[provider.authorization])),
123
+ 'ActionView::Renderer' => TargetMethods.new(:render, Package.build_from_gem('actionview', package_name: 'action_view', labels: %w[mvc.view], optional: true)),
124
+ 'ActionDispatch::Request::Session' => TargetMethods.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.session], optional: true)),
125
+ 'ActionDispatch::Cookies::CookieJar' => TargetMethods.new(%i[[]= clear update delete recycle], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.cookie], optional: true)),
126
+ 'ActionDispatch::Cookies::EncryptedCookieJar' => TargetMethods.new(%i[[]=], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.cookie crypto.encrypt], optional: true)),
127
+ 'CanCan::ControllerAdditions' => TargetMethods.new(%i[authorize! can? cannot?], Package.build_from_gem('cancancan', labels: %w[security.authorization], optional: true)),
128
+ 'CanCan::Ability' => TargetMethods.new(%i[authorize!], Package.build_from_gem('cancancan', labels: %w[security.authorization], optional: true)),
129
+ 'ActionController::Instrumentation' => [
130
+ TargetMethods.new(%i[process_action send_file send_data redirect_to], Package.build_from_gem('actionpack', package_name: 'action_controller', labels: %w[mvc.controller], optional: true)),
131
+ TargetMethods.new(%i[render], Package.build_from_gem('actionpack', package_name: 'action_controller', labels: %w[mvc.view], optional: true)),
132
+ ]
66
133
  }.freeze
67
134
 
68
135
  BUILTIN_METHODS = {
69
- 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
70
- 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
71
- 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
72
- 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
73
- 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
74
- 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http io])),
75
- 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.smtp protocol.email io])),
76
- 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.pop protocol.email io])),
77
- 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.imap protocol.email io])),
78
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization])),
79
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml provider.serialization])),
80
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
81
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
136
+ 'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
137
+ 'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
138
+ 'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
139
+ 'OpenSSL::Cipher' => [
140
+ TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
141
+ TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
142
+ ],
143
+ 'ActiveSupport::Callbacks::CallbackSequence' => [
144
+ TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.before_action])),
145
+ TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.after_action])),
146
+ ],
147
+ 'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, package_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
148
+ 'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
149
+ 'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
150
+ package.handler_class = AppMap::Handler::NetHTTP
151
+ end),
152
+ 'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
153
+ 'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
154
+ # This is happening: Method send_command not found on Net::IMAP
155
+ # 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
156
+ # 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
157
+ 'Psych' => TargetMethods.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
158
+ 'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
159
+ 'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
82
160
  }.freeze
83
161
 
84
- attr_reader :name, :packages, :exclude
162
+ attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_methods
85
163
 
86
- def initialize(name, packages = [], exclude = [])
164
+ def initialize(name, packages, exclude: [], functions: [])
87
165
  @name = name
88
166
  @packages = packages
167
+ @hook_paths = packages.map(&:path)
89
168
  @exclude = exclude
169
+ @builtin_methods = BUILTIN_METHODS
170
+ @functions = functions
171
+ @hooked_methods = HOOKED_METHODS.dup
172
+ functions.each do |func|
173
+ package_options = {}
174
+ package_options[:labels] = func.labels if func.labels
175
+ @hooked_methods[func.cls] ||= []
176
+ @hooked_methods[func.cls] << TargetMethods.new(func.function_names, Package.build_from_path(func.package, package_options))
177
+ end
178
+
179
+ @hooked_methods.each_value do |hooks|
180
+ Array(hooks).each do |hook|
181
+ @hook_paths << hook.package.path if hook.package
182
+ end
183
+ end
90
184
  end
91
185
 
92
186
  class << self
@@ -98,6 +192,16 @@ module AppMap
98
192
 
99
193
  # Loads configuration from a Hash.
100
194
  def load(config_data)
195
+ functions = (config_data['functions'] || []).map do |function_data|
196
+ package = function_data['package']
197
+ cls = function_data['class']
198
+ functions = function_data['function'] || function_data['functions']
199
+ raise 'AppMap class configuration should specify package, class and function(s)' unless package && cls && functions
200
+ functions = Array(functions).map(&:to_sym)
201
+ labels = function_data['label'] || function_data['labels']
202
+ labels = Array(labels).map(&:to_s) if labels
203
+ Function.new(package, cls, labels, functions)
204
+ end
101
205
  packages = (config_data['packages'] || []).map do |package|
102
206
  gem = package['gem']
103
207
  path = package['path']
@@ -112,7 +216,8 @@ module AppMap
112
216
  Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
113
217
  end
114
218
  end.compact
115
- Config.new config_data['name'], packages, config_data['exclude'] || []
219
+ exclude = config_data['exclude'] || []
220
+ Config.new config_data['name'], packages, exclude: exclude, functions: functions
116
221
  end
117
222
  end
118
223
 
@@ -120,58 +225,59 @@ module AppMap
120
225
  {
121
226
  name: name,
122
227
  packages: packages.map(&:to_h),
228
+ functions: @functions.map(&:to_h),
123
229
  exclude: exclude
124
230
  }
125
231
  end
126
232
 
127
- # package_for_method finds the Package, if any, which configures the hook
128
- # for a method.
129
- def package_for_method(method)
130
- package_hooked_by_class(method) || package_hooked_by_source_location(method)
233
+ # Determines if methods defined in a file path should possibly be hooked.
234
+ def path_enabled?(path)
235
+ path = AppMap::Util.normalize_path(path)
236
+ @hook_paths.find { |hook_path| path.index(hook_path) == 0 }
131
237
  end
132
238
 
133
- def package_hooked_by_class(method)
134
- defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
135
- return find_package(defined_class, method_name)
136
- end
239
+ # Looks up a class and method in the config, to find the matching Package configuration.
240
+ # This class is only used after +path_enabled?+ has returned `true`.
241
+ LookupPackage = Struct.new(:config, :cls, :method) do
242
+ def package
243
+ # Global "excludes" configuration can be used to ignore any class/method.
244
+ return if config.never_hook?(cls, method)
137
245
 
138
- def package_hooked_by_source_location(method)
139
- location = method.source_location
140
- location_file, = location
141
- return unless location_file
142
-
143
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
144
- packages.select { |pkg| pkg.path }.find do |pkg|
145
- (location_file.index(pkg.path) == 0) &&
146
- !pkg.exclude.find { |p| location_file.index(p) }
246
+ package_for_code_object || package_for_location
147
247
  end
148
- end
149
248
 
150
- def never_hook?(method)
151
- defined_class, separator, method_name = ::AppMap::Hook.qualify_method_name(method)
152
- return true if exclude.member?(defined_class) || exclude.member?([ defined_class, separator, method_name ].join)
153
- end
249
+ # Hook a method which is specified by class and method name.
250
+ def package_for_code_object
251
+ Array(config.hooked_methods[cls.name])
252
+ .compact
253
+ .find { |hook| hook.include_method?(method.name) }
254
+ &.package
255
+ end
154
256
 
155
- # always_hook? indicates a method that should always be hooked.
156
- def always_hook?(defined_class, method_name)
157
- !!find_package(defined_class, method_name)
158
- end
257
+ # Hook a method which is specified by code location (i.e. path).
258
+ def package_for_location
259
+ location = method.source_location
260
+ location_file, = location
261
+ return unless location_file
159
262
 
160
- # included_by_location? indicates a method whose source location matches a method definition that has been
161
- # configured for inclusion.
162
- def included_by_location?(method)
163
- !!package_for_method(method)
263
+ location_file = AppMap::Util.normalize_path(location_file)
264
+ config
265
+ .packages
266
+ .select { |pkg| pkg.path }
267
+ .find do |pkg|
268
+ (location_file.index(pkg.path) == 0) &&
269
+ !pkg.exclude.find { |p| location_file.index(p) }
270
+ end
271
+ end
164
272
  end
165
273
 
166
- def find_package(defined_class, method_name)
167
- hook = find_hook(defined_class)
168
- return nil unless hook
169
-
170
- Array(hook.method_names).include?(method_name) ? hook.package : nil
274
+ def lookup_package(cls, method)
275
+ LookupPackage.new(self, cls, method).package
171
276
  end
172
277
 
173
- def find_hook(defined_class)
174
- HOOKED_METHODS[defined_class] || BUILTIN_METHODS[defined_class]
278
+ def never_hook?(cls, method)
279
+ _, separator, = ::AppMap::Hook.qualify_method_name(method)
280
+ return true if exclude.member?(cls.name) || exclude.member?([ cls.name, separator, method.name ].join)
175
281
  end
176
282
  end
177
283
  end