appmap 0.44.0 → 0.47.1
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 +4 -4
- data/.releaserc.yml +11 -0
- data/.travis.yml +28 -12
- data/CHANGELOG.md +42 -0
- data/README.md +39 -11
- data/README_CI.md +29 -0
- data/Rakefile +4 -2
- data/appmap.gemspec +5 -1
- data/lib/appmap/class_map.rb +7 -15
- data/lib/appmap/config.rb +203 -95
- 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 +155 -0
- data/lib/appmap/hook.rb +109 -71
- data/lib/appmap/hook/method.rb +6 -8
- data/lib/appmap/railtie.rb +5 -5
- data/lib/appmap/trace.rb +47 -6
- data/lib/appmap/util.rb +41 -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 +74 -11
- data/spec/class_map_spec.rb +3 -3
- data/spec/config_spec.rb +3 -1
- data/spec/hook_spec.rb +12 -66
- data/spec/record_net_http_spec.rb +160 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/util_spec.rb +18 -1
- metadata +16 -10
- data/lib/appmap/rails/request_handler.rb +0 -140
- data/lib/appmap/rails/sql_handler.rb +0 -150
- data/patch +0 -1447
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12dbe41efff7d8fd40a884be376ff1579d961371b780eddf18d4724e7ce5997f
|
4
|
+
data.tar.gz: fb038cfbcc6c2432b780d4555b8948729fd65a2943a4bc1200c7d6943a62f5a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 07a9ec31b08915e1d630a90200393d88db2b85b49b8dc21ffa595e598fa57ff8ed3afa463e3e9dc2ae9485d5dff399eb1724c6aa38cbcceeb0d3c6c0757d0f70
|
7
|
+
data.tar.gz: 3c6e629772604730b6acc14409c98694624cbfcbbc14a661182b6bb5388664b15a918c3e82ca5b55dd1daf6ca142741d0380a3e88c152f5bafcd57cb9b38dd6c
|
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
@@ -16,23 +16,39 @@ before_script:
|
|
16
16
|
|
17
17
|
cache:
|
18
18
|
bundler: true
|
19
|
-
directories:
|
20
|
-
- $HOME/docker
|
21
|
-
|
22
|
-
# https://stackoverflow.com/a/41975912
|
23
|
-
before_cache:
|
24
|
-
# Save tagged docker images
|
25
|
-
- >
|
26
|
-
mkdir -p $HOME/docker && docker images -a --filter='dangling=false' --format '{{.Repository}}:{{.Tag}} {{.ID}}'
|
27
|
-
| xargs -n 2 -t sh -c 'test -e $HOME/docker/$1.tar.gz || docker save $0 | gzip -2 > $HOME/docker/$1.tar.gz'
|
28
19
|
|
29
20
|
before_install:
|
30
|
-
#
|
31
|
-
|
21
|
+
# see https://blog.travis-ci.com/docker-rate-limits
|
22
|
+
# and also https://www.docker.com/blog/what-you-need-to-know-about-upcoming-docker-hub-rate-limiting/
|
23
|
+
# if we do not use authorized account,
|
24
|
+
# the pulls-per-IP quota is shared with other Travis users
|
25
|
+
- >
|
26
|
+
if [ ! -z "$DOCKERHUB_PASSWORD" ] && [ ! -z "$DOCKERHUB_USERNAME" ]; then
|
27
|
+
echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin ;
|
28
|
+
fi
|
32
29
|
|
30
|
+
|
31
|
+
# GEM_ALTERNATIVE_NAME only needed for deployment
|
33
32
|
jobs:
|
34
33
|
include:
|
35
34
|
- stage: test
|
36
35
|
script:
|
37
36
|
- mkdir tmp
|
38
|
-
- bundle exec rake test
|
37
|
+
- GEM_ALTERNATIVE_NAME='' bundle exec rake test
|
38
|
+
|
39
|
+
|
40
|
+
before_deploy:
|
41
|
+
- |
|
42
|
+
nvm install --lts \
|
43
|
+
&& nvm use --lts \
|
44
|
+
&& npm i -g \
|
45
|
+
semantic-release \
|
46
|
+
@semantic-release/git \
|
47
|
+
@semantic-release/changelog \
|
48
|
+
semantic-release-rubygem
|
49
|
+
|
50
|
+
deploy:
|
51
|
+
- provider: script
|
52
|
+
script: ./release.sh
|
53
|
+
on:
|
54
|
+
branch: master
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,45 @@
|
|
1
|
+
## [0.47.1](https://github.com/applandinc/appmap-ruby/compare/v0.47.0...v0.47.1) (2021-05-13)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* Add the proper template function hooks for Rails 6.0.7 ([175f489](https://github.com/applandinc/appmap-ruby/commit/175f489acbaed77ad52a18d805e4b6eeae1abfdb))
|
7
|
+
|
8
|
+
# [0.47.0](https://github.com/applandinc/appmap-ruby/compare/v0.46.0...v0.47.0) (2021-05-13)
|
9
|
+
|
10
|
+
|
11
|
+
### Features
|
12
|
+
|
13
|
+
* Emit swagger-style normalized paths instead of Rails-style ones ([5a93cd7](https://github.com/applandinc/appmap-ruby/commit/5a93cd7096ca195146a84a6733c7d502dbcd0272))
|
14
|
+
|
15
|
+
# [0.46.0](https://github.com/applandinc/appmap-ruby/compare/v0.45.1...v0.46.0) (2021-05-12)
|
16
|
+
|
17
|
+
|
18
|
+
### Features
|
19
|
+
|
20
|
+
* Record view template rendering events and template paths ([973b258](https://github.com/applandinc/appmap-ruby/commit/973b2581b6e2d4e15a1b93331e4e95a88678faae))
|
21
|
+
|
22
|
+
## [0.45.1](https://github.com/applandinc/appmap-ruby/compare/v0.45.0...v0.45.1) (2021-05-04)
|
23
|
+
|
24
|
+
|
25
|
+
### Bug Fixes
|
26
|
+
|
27
|
+
* Optimize instrumentation and load time ([db4a8ce](https://github.com/applandinc/appmap-ruby/commit/db4a8ceed4103a52caafa46626c66f33fbfeac27))
|
28
|
+
|
29
|
+
# [0.45.0](https://github.com/applandinc/appmap-ruby/compare/v0.44.0...v0.45.0) (2021-05-03)
|
30
|
+
|
31
|
+
|
32
|
+
### Bug Fixes
|
33
|
+
|
34
|
+
* Properly name status_code in HTTP server response ([556e87c](https://github.com/applandinc/appmap-ruby/commit/556e87c9a7bf214f6b8714add4f77448fd223d33))
|
35
|
+
|
36
|
+
|
37
|
+
### Features
|
38
|
+
|
39
|
+
* Record http_client_request and http_client_response ([1db32ae](https://github.com/applandinc/appmap-ruby/commit/1db32ae0d26a7f1400b6b814d25b13368f06c158))
|
40
|
+
* Update AppMap format version to 1.5.0 ([061705e](https://github.com/applandinc/appmap-ruby/commit/061705e4619cb881e8edd022ef835183e399e127))
|
41
|
+
* **build:** add deployment via `semantic-release` with automatic publication to rubygems ([9f183de](https://github.com/applandinc/appmap-ruby/commit/9f183de13f405900000c3da979c3a8a5b6e34a24))
|
42
|
+
|
1
43
|
# v0.44.0
|
2
44
|
|
3
45
|
* Support recording and labeling of indivudal functions via `functions:` section in *appmap.yml*.
|
data/README.md
CHANGED
@@ -9,11 +9,11 @@
|
|
9
9
|
- [Minitest](#minitest)
|
10
10
|
- [Cucumber](#cucumber)
|
11
11
|
- [Remote recording](#remote-recording)
|
12
|
-
- [Server process recording](#server-process-recording)
|
13
12
|
- [AppMap for VSCode](#appmap-for-vscode)
|
14
13
|
- [AppMap Swagger](#appmap-swagger)
|
15
14
|
- [Uploading AppMaps](#uploading-appmaps)
|
16
15
|
- [Development](#development)
|
16
|
+
- [Internal architecture](#internal-architecture)
|
17
17
|
- [Running tests](#running-tests)
|
18
18
|
- [Using fixture apps](#using-fixture-apps)
|
19
19
|
- [`test/fixtures`](#testfixtures)
|
@@ -325,25 +325,25 @@ if defined?(AppMap)
|
|
325
325
|
end
|
326
326
|
```
|
327
327
|
|
328
|
-
2. Download and unpack the [AppLand browser extension](https://github.com/applandinc/appland-browser-extension). Install into Chrome using `chrome://extensions/`. Turn on "Developer Mode" and then load the extension using the "Load unpacked" button.
|
328
|
+
2. (optional) Download and unpack the [AppLand browser extension](https://github.com/applandinc/appland-browser-extension). Install into Chrome using `chrome://extensions/`. Turn on "Developer Mode" and then load the extension using the "Load unpacked" button.
|
329
329
|
|
330
|
-
3. Start your Rails application server
|
330
|
+
3. Start your Rails application server, with `APPMAP_RECORD=true`. For example:
|
331
331
|
|
332
332
|
```sh-session
|
333
|
-
$ bundle exec rails server
|
333
|
+
$ APPMAP_RECORD=true bundle exec rails server
|
334
334
|
```
|
335
335
|
|
336
|
-
4.
|
336
|
+
4. Start the recording
|
337
337
|
|
338
|
-
|
339
|
-
|
340
|
-
6. Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
|
338
|
+
Option 1: Open the AppLand browser extension and push `Start`.
|
339
|
+
Option 2: `curl -XPOST localhost:3000/_appmap/record` (be sure and get the port number right)
|
341
340
|
|
342
|
-
|
341
|
+
5. Use your app. For example, perform a login flow, or run through a manual UI test.
|
343
342
|
|
344
|
-
|
343
|
+
6. Finish the recording.
|
345
344
|
|
346
|
-
|
345
|
+
Option 1: Open the AppLand browser extension and push `Stop`. The recording will be transferred to the AppLand website and opened in your browser.
|
346
|
+
Option 2: `curl -XDELETE localhost:3000/_appmap/record > recording.appmap.json` - Saves the recording as a local file.
|
347
347
|
|
348
348
|
|
349
349
|
# AppMap for VSCode
|
@@ -369,6 +369,34 @@ For instructions on uploading, see the documentation of the [AppLand CLI](https:
|
|
369
369
|
# Development
|
370
370
|
[](https://travis-ci.com/applandinc/appmap-ruby)
|
371
371
|
|
372
|
+
## Internal architecture
|
373
|
+
|
374
|
+
**Configuration**
|
375
|
+
|
376
|
+
*appmap.yml* is loaded into an `AppMap::Config`.
|
377
|
+
|
378
|
+
**Hooking**
|
379
|
+
|
380
|
+
Once configuration is loaded, `AppMap::Hook` is enabled. "Hooking" refers to the process of replacing a method
|
381
|
+
with a "hooked" version of the method. The hooked method checks to see if tracing is enabled. If so, it wraps the original
|
382
|
+
method with calls that record the parameters and return value.
|
383
|
+
|
384
|
+
**Builtins**
|
385
|
+
|
386
|
+
`Hook` begins by iterating over builtin classes and modules defined in the `Config`. Builtins include code
|
387
|
+
like `openssl` and `net/http`. This code is not dependent on any external libraries being present, and
|
388
|
+
`appmap` cannot guarantee that it will be loaded before builtins. Therefore, it's necessary to require it and
|
389
|
+
hook it by looking up the classes and modules as constants in the `Object` namespace.
|
390
|
+
|
391
|
+
**User code and gems**
|
392
|
+
|
393
|
+
After hooking builtins, `Hook` attaches a [TracePoint](https://ruby-doc.org/core-2.6/TracePoint.html) to `:begin` events.
|
394
|
+
This TracePoint is notified each time a new class or module is being evaluated. When this happens, `Hook` uses the `Config`
|
395
|
+
to determine whether any code within the evaluated file is configured for hooking. If so, a `TracePoint` is attached to
|
396
|
+
`:end` events. Each `:end` event is fired when a class or module definition is completed. When this happens, the `Hook` enumerates
|
397
|
+
the public methods of the class or module, hooking the ones that are targeted by the `Config`. Once the `:end` TracePoint leaves
|
398
|
+
the scope of the `:begin`, the `:end` TracePoint is disabled.
|
399
|
+
|
372
400
|
## Running tests
|
373
401
|
|
374
402
|
Before running tests, configure `local.appmap` to point to your local `appmap-ruby` directory.
|
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}
|
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
|
-
|
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']
|
data/lib/appmap/class_map.rb
CHANGED
@@ -82,16 +82,13 @@ module AppMap
|
|
82
82
|
protected
|
83
83
|
|
84
84
|
def add_function(root, method)
|
85
|
-
package = method.package
|
86
|
-
static = method.static
|
87
|
-
|
88
85
|
object_infos = [
|
89
86
|
{
|
90
|
-
name: package
|
87
|
+
name: method.package,
|
91
88
|
type: 'package'
|
92
89
|
}
|
93
90
|
]
|
94
|
-
object_infos += method.
|
91
|
+
object_infos += method.class_name.split('::').map do |name|
|
95
92
|
{
|
96
93
|
name: name,
|
97
94
|
type: 'class'
|
@@ -100,7 +97,7 @@ module AppMap
|
|
100
97
|
function_info = {
|
101
98
|
name: method.name,
|
102
99
|
type: 'function',
|
103
|
-
static: static
|
100
|
+
static: method.static
|
104
101
|
}
|
105
102
|
location = method.source_location
|
106
103
|
|
@@ -108,20 +105,15 @@ module AppMap
|
|
108
105
|
if location
|
109
106
|
location_file, lineno = location
|
110
107
|
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
111
|
-
[ location_file, lineno ].join(':')
|
108
|
+
[ location_file, lineno ].compact.join(':')
|
112
109
|
else
|
113
|
-
[ method.
|
110
|
+
[ method.class_name, method.static ? '.' : '#', method.name ].join
|
114
111
|
end
|
115
112
|
|
116
|
-
comment =
|
117
|
-
method.comment
|
118
|
-
rescue MethodSource::SourceNotFoundError
|
119
|
-
nil
|
120
|
-
end
|
121
|
-
|
113
|
+
comment = method.comment
|
122
114
|
function_info[:comment] = comment unless comment.blank?
|
123
115
|
|
124
|
-
function_info[:labels] = parse_labels(comment) + (
|
116
|
+
function_info[:labels] = parse_labels(comment) + (method.labels || [])
|
125
117
|
object_infos << function_info
|
126
118
|
|
127
119
|
parent = root
|
data/lib/appmap/config.rb
CHANGED
@@ -1,8 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'appmap/handler/net_http'
|
4
|
+
require 'appmap/handler/rails/template'
|
5
|
+
|
3
6
|
module AppMap
|
4
7
|
class Config
|
8
|
+
# Specifies a code +path+ to be mapped.
|
9
|
+
# Options:
|
10
|
+
#
|
11
|
+
# * +gem+ may indicate a gem name that "owns" the path
|
12
|
+
# * +package_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
|
13
|
+
# builtins, or when the path to be required is not automatically required when bundler requires the gem.
|
14
|
+
# * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
|
15
|
+
# * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
|
16
|
+
# specific functions, via TargetMethods.
|
17
|
+
# * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
|
5
18
|
Package = Struct.new(:path, :gem, :package_name, :exclude, :labels, :shallow) do
|
19
|
+
# This is for internal use only.
|
20
|
+
private_methods :gem
|
21
|
+
|
22
|
+
# Specifies the class that will convert code events into event objects.
|
23
|
+
attr_writer :handler_class
|
24
|
+
|
25
|
+
def handler_class
|
26
|
+
require 'appmap/handler/function'
|
27
|
+
@handler_class || AppMap::Handler::Function
|
28
|
+
end
|
29
|
+
|
6
30
|
# Indicates that only the entry points to a package will be recorded.
|
7
31
|
# Once the code has entered a package, subsequent calls within the package will not be
|
8
32
|
# recorded unless the code leaves the package and re-enters it.
|
@@ -11,25 +35,36 @@ module AppMap
|
|
11
35
|
end
|
12
36
|
|
13
37
|
class << self
|
38
|
+
# Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
|
39
|
+
# in appmap.yml. Also used for mapping specific methods via TargetMethods.
|
14
40
|
def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
|
15
41
|
Package.new(path, nil, package_name, exclude, labels, shallow)
|
16
42
|
end
|
17
43
|
|
18
|
-
|
19
|
-
|
44
|
+
# Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
|
45
|
+
# a builtin.
|
46
|
+
def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [], optional: false, force: false)
|
47
|
+
if !force && %w[method_source activesupport].member?(gem)
|
20
48
|
warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
|
21
49
|
return
|
22
50
|
end
|
23
|
-
|
51
|
+
path = gem_path(gem, optional)
|
52
|
+
if path
|
53
|
+
Package.new(path, gem, package_name, exclude, labels, shallow)
|
54
|
+
else
|
55
|
+
warn "#{gem} is not available in the bundle" if AppMap::Hook::LOG
|
56
|
+
end
|
24
57
|
end
|
25
58
|
|
26
59
|
private_class_method :new
|
27
60
|
|
28
61
|
protected
|
29
62
|
|
30
|
-
def gem_path(gem)
|
31
|
-
gemspec = Gem.loaded_specs[gem]
|
32
|
-
|
63
|
+
def gem_path(gem, optional)
|
64
|
+
gemspec = Gem.loaded_specs[gem]
|
65
|
+
# This exception will notify a user that their appmap.yml contains non-existent gems.
|
66
|
+
raise "Gem #{gem.inspect} not found" unless gemspec || optional
|
67
|
+
gemspec ? gemspec.gem_dir : nil
|
33
68
|
end
|
34
69
|
end
|
35
70
|
|
@@ -42,6 +77,7 @@ module AppMap
|
|
42
77
|
path: path,
|
43
78
|
package_name: package_name,
|
44
79
|
gem: gem,
|
80
|
+
handler_class: handler_class.name,
|
45
81
|
exclude: exclude.blank? ? nil : exclude,
|
46
82
|
labels: labels.blank? ? nil : labels,
|
47
83
|
shallow: shallow
|
@@ -49,7 +85,33 @@ module AppMap
|
|
49
85
|
end
|
50
86
|
end
|
51
87
|
|
52
|
-
|
88
|
+
# Identifies specific methods within a package which should be hooked.
|
89
|
+
class TargetMethods # :nodoc:
|
90
|
+
attr_reader :method_names, :package
|
91
|
+
|
92
|
+
def initialize(method_names, package)
|
93
|
+
@method_names = method_names
|
94
|
+
@package = package
|
95
|
+
end
|
96
|
+
|
97
|
+
def include_method?(method_name)
|
98
|
+
Array(method_names).include?(method_name)
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_h
|
102
|
+
{
|
103
|
+
package: package.name,
|
104
|
+
method_names: method_names
|
105
|
+
}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
private_constant :TargetMethods
|
109
|
+
|
110
|
+
# Function represents a specific function configured for hooking by the +functions+
|
111
|
+
# entry in appmap.yml. When the Config is initialized, each Function is converted into
|
112
|
+
# a Package and TargetMethods. It's called a Function rather than a Method, because Function
|
113
|
+
# is the AppMap terminology.
|
114
|
+
Function = Struct.new(:package, :cls, :labels, :function_names) do # :nodoc:
|
53
115
|
def to_h
|
54
116
|
{
|
55
117
|
package: package,
|
@@ -59,78 +121,127 @@ module AppMap
|
|
59
121
|
}.compact
|
60
122
|
end
|
61
123
|
end
|
124
|
+
private_constant :Function
|
62
125
|
|
63
|
-
|
64
|
-
|
126
|
+
ClassTargetMethods = Struct.new(:cls, :target_methods) # :nodoc:
|
127
|
+
private_constant :ClassTargetMethods
|
65
128
|
|
66
|
-
|
67
|
-
|
68
|
-
|
129
|
+
MethodHook = Struct.new(:cls, :method_names, :labels) # :nodoc:
|
130
|
+
private_constant :MethodHook
|
131
|
+
|
132
|
+
class << self
|
133
|
+
def package_hooks(gem_name, methods, handler_class: nil, package_name: nil)
|
134
|
+
Array(methods).map do |method|
|
135
|
+
package = Package.build_from_gem(gem_name, package_name: package_name, labels: method.labels, shallow: false, optional: true)
|
136
|
+
next unless package
|
137
|
+
|
138
|
+
package.handler_class = handler_class if handler_class
|
139
|
+
ClassTargetMethods.new(method.cls, TargetMethods.new(Array(method.method_names), package))
|
140
|
+
end.compact
|
69
141
|
end
|
70
142
|
|
71
|
-
def
|
72
|
-
|
73
|
-
package: package.name,
|
74
|
-
method_names: method_names
|
75
|
-
}
|
143
|
+
def method_hook(cls, method_names, labels)
|
144
|
+
MethodHook.new(cls, method_names, labels)
|
76
145
|
end
|
77
146
|
end
|
78
147
|
|
79
|
-
|
148
|
+
# Hook well-known functions. When a function configured here is available in the bundle, it will be hooked with the
|
149
|
+
# predefined labels specified here. If any of these hooks are not desired, they can be disabled in the +exclude+ section
|
150
|
+
# of appmap.yml.
|
151
|
+
METHOD_HOOKS = [
|
152
|
+
package_hooks('actionview',
|
153
|
+
[
|
154
|
+
method_hook('ActionView::Renderer', :render, %w[mvc.view]),
|
155
|
+
method_hook('ActionView::TemplateRenderer', :render, %w[mvc.view]),
|
156
|
+
method_hook('ActionView::PartialRenderer', :render, %w[mvc.view])
|
157
|
+
],
|
158
|
+
handler_class: AppMap::Handler::Rails::Template::RenderHandler,
|
159
|
+
package_name: 'action_view'
|
160
|
+
),
|
161
|
+
package_hooks('actionview',
|
162
|
+
[
|
163
|
+
method_hook('ActionView::Resolver', %i[find_all find_all_anywhere], %w[mvc.template.resolver])
|
164
|
+
],
|
165
|
+
handler_class: AppMap::Handler::Rails::Template::ResolverHandler,
|
166
|
+
package_name: 'action_view'
|
167
|
+
),
|
168
|
+
package_hooks('actionpack',
|
169
|
+
[
|
170
|
+
method_hook('ActionDispatch::Request::Session', %i[destroy [] dig values []= clear update delete fetch merge], %w[http.session]),
|
171
|
+
method_hook('ActionDispatch::Cookies::CookieJar', %i[[]= clear update delete recycle], %w[http.session]),
|
172
|
+
method_hook('ActionDispatch::Cookies::EncryptedCookieJar', %i[[]= clear update delete recycle], %w[http.cookie crypto.encrypt])
|
173
|
+
],
|
174
|
+
package_name: 'action_dispatch'
|
175
|
+
),
|
176
|
+
package_hooks('cancancan',
|
177
|
+
[
|
178
|
+
method_hook('CanCan::ControllerAdditions', %i[authorize! can? cannot?], %w[security.authorization]),
|
179
|
+
method_hook('CanCan::Ability', %i[authorize?], %w[security.authorization])
|
180
|
+
]
|
181
|
+
),
|
182
|
+
package_hooks('actionpack',
|
183
|
+
[
|
184
|
+
method_hook('ActionController::Instrumentation', %i[process_action send_file send_data redirect_to], %w[mvc.controller])
|
185
|
+
],
|
186
|
+
package_name: 'action_controller'
|
187
|
+
)
|
188
|
+
].flatten.freeze
|
80
189
|
|
81
|
-
|
82
|
-
# package and labels that should be applied to them.
|
83
|
-
HOOKED_METHODS = {
|
84
|
-
'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[crypto.secure_compare])),
|
85
|
-
'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', labels: %w[mvc.view])),
|
86
|
-
'ActionDispatch::Request::Session' => Hook.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_path('action_pack', labels: %w[http.session])),
|
87
|
-
'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[http.cookie])),
|
88
|
-
'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[http.cookie crypto.encrypt])),
|
89
|
-
'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[security.authorization])),
|
90
|
-
'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[security.authorization])),
|
91
|
-
'ActionController::Instrumentation' => [
|
92
|
-
Hook.new(%i[process_action send_file send_data redirect_to], Package.build_from_path('action_view', labels: %w[mvc.controller])),
|
93
|
-
Hook.new(%i[render], Package.build_from_path('action_view', labels: %w[mvc.view])),
|
94
|
-
]
|
95
|
-
}.freeze
|
190
|
+
OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
|
96
191
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
'OpenSSL::
|
192
|
+
# Hook functions which are builtin to Ruby. Because they are builtins, they may be loaded before appmap.
|
193
|
+
# Therefore, we can't rely on TracePoint to report the loading of this code.
|
194
|
+
BUILTIN_HOOKS = {
|
195
|
+
'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
|
196
|
+
'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
|
197
|
+
'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
|
101
198
|
'OpenSSL::Cipher' => [
|
102
|
-
|
103
|
-
|
199
|
+
TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
|
200
|
+
TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
|
104
201
|
],
|
105
202
|
'ActiveSupport::Callbacks::CallbackSequence' => [
|
106
|
-
|
107
|
-
|
203
|
+
TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.before_action])),
|
204
|
+
TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.after_action])),
|
108
205
|
],
|
109
|
-
'
|
110
|
-
'
|
111
|
-
'Net::
|
112
|
-
|
113
|
-
|
114
|
-
'
|
115
|
-
'
|
116
|
-
|
117
|
-
'
|
206
|
+
'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, package_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
|
207
|
+
'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
|
208
|
+
'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
|
209
|
+
package.handler_class = AppMap::Handler::NetHTTP
|
210
|
+
end),
|
211
|
+
'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
|
212
|
+
'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
|
213
|
+
# This is happening: Method send_command not found on Net::IMAP
|
214
|
+
# 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
|
215
|
+
# 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
|
216
|
+
'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])),
|
217
|
+
'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
|
218
|
+
'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
|
118
219
|
}.freeze
|
119
220
|
|
120
|
-
attr_reader :name, :packages, :exclude, :
|
221
|
+
attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_hooks
|
121
222
|
|
122
223
|
def initialize(name, packages, exclude: [], functions: [])
|
123
224
|
@name = name
|
124
225
|
@packages = packages
|
226
|
+
@hook_paths = Set.new(packages.map(&:path))
|
125
227
|
@exclude = exclude
|
126
|
-
@
|
228
|
+
@builtin_hooks = BUILTIN_HOOKS
|
127
229
|
@functions = functions
|
128
|
-
|
230
|
+
|
231
|
+
@hooked_methods = METHOD_HOOKS.each_with_object(Hash.new { |h,k| h[k] = [] }) do |cls_target_methods, hooked_methods|
|
232
|
+
hooked_methods[cls_target_methods.cls] << cls_target_methods.target_methods
|
233
|
+
end
|
234
|
+
|
129
235
|
functions.each do |func|
|
130
236
|
package_options = {}
|
131
237
|
package_options[:labels] = func.labels if func.labels
|
132
|
-
@hooked_methods[func.cls]
|
133
|
-
|
238
|
+
@hooked_methods[func.cls] << TargetMethods.new(func.function_names, Package.build_from_path(func.package, package_options))
|
239
|
+
end
|
240
|
+
|
241
|
+
@hooked_methods.each_value do |hooks|
|
242
|
+
Array(hooks).each do |hook|
|
243
|
+
@hook_paths << hook.package.path
|
244
|
+
end
|
134
245
|
end
|
135
246
|
end
|
136
247
|
|
@@ -181,57 +292,54 @@ module AppMap
|
|
181
292
|
}
|
182
293
|
end
|
183
294
|
|
184
|
-
#
|
185
|
-
|
186
|
-
|
187
|
-
|
295
|
+
# Determines if methods defined in a file path should possibly be hooked.
|
296
|
+
def path_enabled?(path)
|
297
|
+
path = AppMap::Util.normalize_path(path)
|
298
|
+
@hook_paths.find { |hook_path| path.index(hook_path) == 0 }
|
188
299
|
end
|
189
300
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
301
|
+
# Looks up a class and method in the config, to find the matching Package configuration.
|
302
|
+
# This class is only used after +path_enabled?+ has returned `true`.
|
303
|
+
LookupPackage = Struct.new(:config, :cls, :method) do
|
304
|
+
def package
|
305
|
+
# Global "excludes" configuration can be used to ignore any class/method.
|
306
|
+
return if config.never_hook?(cls, method)
|
194
307
|
|
195
|
-
|
196
|
-
location = method.source_location
|
197
|
-
location_file, = location
|
198
|
-
return unless location_file
|
199
|
-
|
200
|
-
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
201
|
-
packages.select { |pkg| pkg.path }.find do |pkg|
|
202
|
-
(location_file.index(pkg.path) == 0) &&
|
203
|
-
!pkg.exclude.find { |p| location_file.index(p) }
|
308
|
+
package_for_code_object || package_for_location
|
204
309
|
end
|
205
|
-
end
|
206
310
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
311
|
+
# Hook a method which is specified by class and method name.
|
312
|
+
def package_for_code_object
|
313
|
+
Array(config.hooked_methods[cls.name])
|
314
|
+
.compact
|
315
|
+
.find { |hook| hook.include_method?(method.name) }
|
316
|
+
&.package
|
317
|
+
end
|
211
318
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
319
|
+
# Hook a method which is specified by code location (i.e. path).
|
320
|
+
def package_for_location
|
321
|
+
location = method.source_location
|
322
|
+
location_file, = location
|
323
|
+
return unless location_file
|
216
324
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
325
|
+
location_file = AppMap::Util.normalize_path(location_file)
|
326
|
+
config
|
327
|
+
.packages
|
328
|
+
.select { |pkg| pkg.path }
|
329
|
+
.find do |pkg|
|
330
|
+
(location_file.index(pkg.path) == 0) &&
|
331
|
+
!pkg.exclude.find { |p| location_file.index(p) }
|
332
|
+
end
|
333
|
+
end
|
221
334
|
end
|
222
335
|
|
223
|
-
def
|
224
|
-
|
225
|
-
return nil unless hooks
|
226
|
-
|
227
|
-
hook = Array(hooks).find do |hook|
|
228
|
-
Array(hook.method_names).include?(method_name)
|
229
|
-
end
|
230
|
-
hook ? hook.package : nil
|
336
|
+
def lookup_package(cls, method)
|
337
|
+
LookupPackage.new(self, cls, method).package
|
231
338
|
end
|
232
339
|
|
233
|
-
def
|
234
|
-
|
340
|
+
def never_hook?(cls, method)
|
341
|
+
_, separator, = ::AppMap::Hook.qualify_method_name(method)
|
342
|
+
return true if exclude.member?(cls.name) || exclude.member?([ cls.name, separator, method.name ].join)
|
235
343
|
end
|
236
344
|
end
|
237
345
|
end
|