appmap 0.42.0 → 0.45.1
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 +23 -2
- data/CHANGELOG.md +42 -0
- data/README.md +65 -6
- data/README_CI.md +29 -0
- data/Rakefile +4 -2
- data/appmap.gemspec +5 -3
- data/lib/appmap.rb +4 -7
- data/lib/appmap/class_map.rb +7 -10
- data/lib/appmap/command/record.rb +1 -1
- data/lib/appmap/config.rb +173 -67
- data/lib/appmap/cucumber.rb +1 -1
- data/lib/appmap/event.rb +18 -0
- data/lib/appmap/handler/function.rb +19 -0
- data/lib/appmap/handler/net_http.rb +107 -0
- data/lib/appmap/hook.rb +112 -56
- data/lib/appmap/hook/method.rb +5 -7
- data/lib/appmap/middleware/remote_recording.rb +1 -1
- data/lib/appmap/minitest.rb +22 -20
- data/lib/appmap/rails/request_handler.rb +30 -17
- data/lib/appmap/record.rb +1 -1
- data/lib/appmap/rspec.rb +23 -21
- data/lib/appmap/trace.rb +2 -1
- data/lib/appmap/util.rb +47 -2
- data/lib/appmap/version.rb +2 -2
- data/release.sh +17 -0
- data/spec/abstract_controller_base_spec.rb +77 -30
- data/spec/class_map_spec.rb +3 -11
- 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/fixtures/rails5_users_app/Gemfile +7 -3
- data/spec/fixtures/rails5_users_app/app/controllers/api/users_controller.rb +2 -0
- data/spec/fixtures/rails5_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails5_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails5_users_app/create_app +8 -2
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails5_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails5_users_app/spec/rails_helper.rb +3 -9
- data/spec/fixtures/rails6_users_app/Gemfile +5 -4
- data/spec/fixtures/rails6_users_app/app/controllers/api/users_controller.rb +1 -0
- data/spec/fixtures/rails6_users_app/app/controllers/users_controller.rb +9 -1
- data/spec/fixtures/rails6_users_app/config/application.rb +2 -0
- data/spec/fixtures/rails6_users_app/create_app +8 -2
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_api_spec.rb +13 -0
- data/spec/fixtures/rails6_users_app/spec/controllers/users_controller_spec.rb +2 -2
- data/spec/fixtures/rails6_users_app/spec/rails_helper.rb +3 -9
- data/spec/hook_spec.rb +141 -20
- data/spec/record_net_http_spec.rb +160 -0
- data/spec/record_sql_rails_pg_spec.rb +1 -1
- data/spec/spec_helper.rb +16 -0
- 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 +17 -12
- data/exe/appmap +0 -154
- data/test/cli_test.rb +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b91b79723565f45d9d59ce341a9944a3a4e1c853bc77c1b176dc7b26ede9df22
|
4
|
+
data.tar.gz: f9be88ae7e83b801f66ada4087a06923ad9fa4d10e498f1c8b07fe0e301552d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
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,
|
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/
|
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
|
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}
|
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']
|
@@ -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
|
92
|
-
ClassMap.build_from_methods(methods
|
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
|
data/lib/appmap/class_map.rb
CHANGED
@@ -71,17 +71,17 @@ module AppMap
|
|
71
71
|
end
|
72
72
|
|
73
73
|
class << self
|
74
|
-
def build_from_methods(methods
|
74
|
+
def build_from_methods(methods)
|
75
75
|
root = Types::Root.new
|
76
76
|
methods.each do |method|
|
77
|
-
add_function root, method
|
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
|
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
|
-
|
117
|
-
|
116
|
+
comment = begin
|
117
|
+
method.comment
|
118
118
|
rescue MethodSource::SourceNotFoundError
|
119
|
-
|
119
|
+
nil
|
120
120
|
end
|
121
121
|
|
122
|
-
|
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
|
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
|
-
|
19
|
-
|
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
|
-
|
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]
|
32
|
-
|
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
|
-
|
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:
|
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
|
-
'
|
61
|
-
'
|
62
|
-
'ActionDispatch::Cookies::CookieJar' =>
|
63
|
-
'ActionDispatch::Cookies::EncryptedCookieJar' =>
|
64
|
-
'CanCan::ControllerAdditions' =>
|
65
|
-
'CanCan::Ability' =>
|
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' =>
|
70
|
-
'OpenSSL::X509::Request' =>
|
71
|
-
'OpenSSL::PKCS5' =>
|
72
|
-
'OpenSSL::Cipher' =>
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
'
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
'
|
81
|
-
'
|
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
|
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
|
-
|
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
|
-
#
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
167
|
-
|
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
|
174
|
-
|
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
|