rails_watcher 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +56 -0
- data/Rakefile +34 -0
- data/lib/rails_watcher.rb +16 -0
- data/lib/rails_watcher/call_stack.rb +89 -0
- data/lib/rails_watcher/configuration.rb +39 -0
- data/lib/rails_watcher/const_modifier.rb +81 -0
- data/lib/rails_watcher/default_instance_handler.rb +34 -0
- data/lib/rails_watcher/engine.rb +74 -0
- data/lib/rails_watcher/patches.rb +90 -0
- data/lib/rails_watcher/version.rb +3 -0
- data/lib/rails_watcher/watcher_middleware.rb +16 -0
- data/lib/tasks/configuration.rb.tmp +68 -0
- data/lib/tasks/rails_watcher_tasks.rake +9 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d8df209f203deec625e15a9826fe1a1a922150501df153d359b2a6f7b807f524
|
4
|
+
data.tar.gz: 4c6dcb7f591660a6c181e833c86081b96bc8264440cde3fc39d3539849570477
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2fc3665f568e9c19ed8de9b1ddb5c7646095fbf21db7617971e41250ed24197b5a435e1c976f818744e2f2bf65baa1ab32697444d13ba7c0b52299f0f2edfd63
|
7
|
+
data.tar.gz: b495b98b57ce2f8f29b3176b619ae0d8a3db6bfebe7b696140579913d2a831a3cf7fecd0f946f7f596fa491c1eafeb1a395ffd2be02247095eca962828d24d7b
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Zhang Kang
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Rails Watcher
|
2
|
+
Help you benchmark / analyze / understand your Rails application.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
Add `rails_watcher` to your `Gemfile`.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# rails_watcher will slow down your application, suggest only enable on development environment.
|
9
|
+
gem 'rails_watcher', require: ENV['RAILS_WATCHER'], group: :development
|
10
|
+
```
|
11
|
+
|
12
|
+
Start your Rails application with `RAILS_WATCHER`.
|
13
|
+
```sh
|
14
|
+
$ RAILS_WATCHER=true rails s
|
15
|
+
```
|
16
|
+
|
17
|
+
Then make some requests to your application, if the response time slow than 10 ms (could be configured), `Rails Watcher` will capture the all details for your request.
|
18
|
+
|
19
|
+
By default, `Rails Watcher` will save captured data to `#{Rails.root}/tmp/rails_watcher`.
|
20
|
+
|
21
|
+
## View Result
|
22
|
+
If you use default configuration of `Rails Watcher`, you could use `rails_watcher_viewer` to view result:
|
23
|
+
```sh
|
24
|
+
$ gem install -N rails_watcher_viewer
|
25
|
+
$ cd application_folder/tmp/rails_watcher
|
26
|
+
$ rails_watcher_viewer
|
27
|
+
```
|
28
|
+
View `localhost:4567`, you could see the list of your requests:
|
29
|
+
[!list]()
|
30
|
+
You can navigate to details page, the `Expensive methods` table show the slow methods ordered by `net cost`:
|
31
|
+
[!detail1]()
|
32
|
+
### How to calculate `net cost`
|
33
|
+
```ruby
|
34
|
+
@a = 1
|
35
|
+
def sample_method
|
36
|
+
sleep @a
|
37
|
+
inner_method()
|
38
|
+
@a += 1
|
39
|
+
end
|
40
|
+
|
41
|
+
def inner_method
|
42
|
+
sleep @a * 3
|
43
|
+
end
|
44
|
+
|
45
|
+
sample_method() # will take 1 + 3 = 4 seconds
|
46
|
+
sample_method() # will take 2 + 6 = 8 seconds
|
47
|
+
```
|
48
|
+
The net cost of sample_method should be 1 + 2 = 3 seconds, the total cost of sample_method should be 4 + 8 = 12 seconds.
|
49
|
+
|
50
|
+
[!detail2]()
|
51
|
+
The `Call Stack` section show the call stacks of your application.
|
52
|
+
Please notice that, it only show the methods inside your application plus any methods you want to watch, which means the Ruby build-in methods and methods inside any gem will not be captured by default. It is great helpful for trouble shooting.
|
53
|
+
|
54
|
+
|
55
|
+
## License
|
56
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'RailsWatcher'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rails_watcher/engine'
|
3
|
+
require 'rails_watcher/configuration'
|
4
|
+
require 'rails_watcher/const_modifier'
|
5
|
+
require 'rails_watcher/call_stack'
|
6
|
+
require 'rails_watcher/patches'
|
7
|
+
require 'rails_watcher/watcher_middleware'
|
8
|
+
|
9
|
+
module RailsWatcher
|
10
|
+
|
11
|
+
autoload :DefaultInstanceHandler, 'rails_watcher/default_instance_handler'
|
12
|
+
|
13
|
+
def self.my_watch_begins
|
14
|
+
Kernel.singleton_class.prepend Patches::KernelLoad
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsWatcher
|
3
|
+
class CallStack
|
4
|
+
class << self
|
5
|
+
def get_instance
|
6
|
+
Thread.current[:rails_watcher_call_stack_instance]
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_instance request_path
|
10
|
+
Thread.current[:rails_watcher_call_stack_instance] = self.new request_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def clear_instance_and_log duration
|
14
|
+
instance = self.get_instance
|
15
|
+
if duration > RailsWatcher.configuration.request_duration_threshold &&
|
16
|
+
RailsWatcher.configuration.ignored_request_path.all? { |path| path !~ instance.request_path }
|
17
|
+
|
18
|
+
if instance.stack.present?
|
19
|
+
instance.duration = duration
|
20
|
+
handler = RailsWatcher.configuration.instance_handler
|
21
|
+
handler = handler.constantize if handler.is_a? String
|
22
|
+
|
23
|
+
handler.log instance
|
24
|
+
end
|
25
|
+
end
|
26
|
+
Thread.current[:rails_watcher_call_stack_instance] = nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :request_path, :stack, :method_call_table
|
31
|
+
attr_reader :db_query_table, :read_cache_table, :render_stack
|
32
|
+
attr_accessor :duration
|
33
|
+
|
34
|
+
def initialize request_path
|
35
|
+
@request_path = request_path
|
36
|
+
@stack = []
|
37
|
+
@current_stack = []
|
38
|
+
@method_call_table = {}
|
39
|
+
@db_query_table = {}
|
40
|
+
@read_cache_table = {}
|
41
|
+
@render_stack = []
|
42
|
+
end
|
43
|
+
|
44
|
+
# def save
|
45
|
+
# folder_name = "#{Time.now.strftime("%Y%m%d_%H%M%S")}|#{@duration}|#{@request_path.gsub("/", "\\")}"
|
46
|
+
# path = File.join(RailsWatcher.configuration.output_path, folder_name)
|
47
|
+
# FileUtils.mkdir_p path
|
48
|
+
# %w[stack method_call_table db_query_table read_cache_table render_stack].each do |var|
|
49
|
+
# filename = File.join path, "#{var}.json"
|
50
|
+
# File.open(filename, 'w') { |f| f.puts instance_variable_get(:"@#{var}").to_json }
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
|
54
|
+
def log_method_call tag, &blk
|
55
|
+
id = SecureRandom.hex(10)
|
56
|
+
|
57
|
+
@method_call_table[id] = { tag: tag, children: [] }
|
58
|
+
parent_method_call = @current_stack.last
|
59
|
+
if parent_method_call
|
60
|
+
@method_call_table[parent_method_call][:children] << id
|
61
|
+
else
|
62
|
+
@stack << id
|
63
|
+
end
|
64
|
+
|
65
|
+
@current_stack.push id
|
66
|
+
duration = Benchmark.ms &blk
|
67
|
+
@method_call_table[id][:duration] = duration
|
68
|
+
@current_stack.delete id
|
69
|
+
end
|
70
|
+
|
71
|
+
def log_render_stack event_type, duration, payload
|
72
|
+
@render_stack << [event_type, duration, payload]
|
73
|
+
end
|
74
|
+
|
75
|
+
def log_db_query duration, payload
|
76
|
+
current_method_call = @current_stack.last
|
77
|
+
@db_query_table[current_method_call] ||= []
|
78
|
+
payload[:duration] = duration
|
79
|
+
@db_query_table[current_method_call] << payload
|
80
|
+
end
|
81
|
+
|
82
|
+
def log_catche_read duration, payload
|
83
|
+
current_method_call = @current_stack.last
|
84
|
+
@read_cache_table[current_method_call] ||= []
|
85
|
+
payload[:duration] = duration
|
86
|
+
@read_cache_table[current_method_call] << payload
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsWatcher
|
3
|
+
|
4
|
+
class Configuration
|
5
|
+
attr_accessor *%i[
|
6
|
+
ignored_files
|
7
|
+
ignored_constants
|
8
|
+
ignored_paths
|
9
|
+
ignored_methods
|
10
|
+
ignored_request_path
|
11
|
+
file_constant_mapping
|
12
|
+
request_duration_threshold
|
13
|
+
rails_methods_i_want_to_watch
|
14
|
+
instance_handler
|
15
|
+
]
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@ignored_files = []
|
19
|
+
@ignored_constants = []
|
20
|
+
@ignored_paths = []
|
21
|
+
@ignored_methods = {}
|
22
|
+
@file_constant_mapping = {}
|
23
|
+
@ignored_request_path = []
|
24
|
+
@request_duration_threshold = 10 # ms
|
25
|
+
@rails_methods_i_want_to_watch = {}
|
26
|
+
@instance_handler = "RailsWatcher::DefaultInstanceHandler"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.configuration
|
31
|
+
@@configuration ||= RailsWatcher::Configuration.new
|
32
|
+
if block_given?
|
33
|
+
yield @@configuration
|
34
|
+
else
|
35
|
+
@@configuration
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsWatcher
|
3
|
+
class ConstModifier
|
4
|
+
|
5
|
+
def self.modify const_name, file_path
|
6
|
+
@instance ||= self.new
|
7
|
+
@instance.modify const_name, file_path
|
8
|
+
end
|
9
|
+
|
10
|
+
def modify const_name, file_path
|
11
|
+
const = const_name.constantize rescue return
|
12
|
+
configuration = RailsWatcher.configuration
|
13
|
+
|
14
|
+
ignored_methods = configuration.ignored_methods.with_indifferent_access
|
15
|
+
|
16
|
+
instance_methods_module = Module.new do
|
17
|
+
%w[public private protected].each do |method_type|
|
18
|
+
const.send(:"#{method_type}_instance_methods", false).each do |method_name|
|
19
|
+
source_code_file_path, _line_no = const.instance_method(method_name).source_location
|
20
|
+
next unless source_code_file_path == file_path
|
21
|
+
next if ignored_methods.dig(const_name, method_type)&.include?(method_name)
|
22
|
+
next if ignored_methods["WeirdMethods"]&.include?(method_name)
|
23
|
+
|
24
|
+
define_method method_name do |*args, &blk|
|
25
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
26
|
+
if call_stack
|
27
|
+
ret = nil
|
28
|
+
method_tag = "#{const_name}##{method_name}"
|
29
|
+
call_stack.log_method_call method_tag do
|
30
|
+
ret = super(*args, &blk)
|
31
|
+
end
|
32
|
+
ret
|
33
|
+
else
|
34
|
+
super(*args, &blk)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
__send__(method_type, method_name)
|
38
|
+
end
|
39
|
+
end # %w[public private protected].each do |method_type|
|
40
|
+
end # instance_methods_module = Module.new do
|
41
|
+
|
42
|
+
const.prepend instance_methods_module
|
43
|
+
|
44
|
+
# modifiy class methodes
|
45
|
+
private_class_methods = []
|
46
|
+
|
47
|
+
const.singleton_class.class_eval do
|
48
|
+
%w[public private].each do |method_type|
|
49
|
+
const.send(:"#{method_type}_methods", false).each do |method_name|
|
50
|
+
source_code_file_path, _line_no = const.method(method_name).source_location
|
51
|
+
next unless source_code_file_path == file_path
|
52
|
+
next if ignored_methods.dig(const_name, :"class_#{method_type}")&.include?(method_name)
|
53
|
+
next if ignored_methods["WeirdMethods"]&.include?(method_name)
|
54
|
+
|
55
|
+
aliased_method_name = :"origin_#{method_name}__rails_watcher"
|
56
|
+
alias_method aliased_method_name ,method_name
|
57
|
+
|
58
|
+
define_method method_name do |*args, &blk|
|
59
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
60
|
+
if call_stack
|
61
|
+
ret = nil
|
62
|
+
method_tag = "#{const_name}.#{method_name}"
|
63
|
+
call_stack.log_method_call method_tag do
|
64
|
+
ret = __send__(aliased_method_name, *args, &blk)
|
65
|
+
end
|
66
|
+
return ret
|
67
|
+
else
|
68
|
+
__send__(aliased_method_name, *args, &blk)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
private_class_methods << method_name if method_type == :private
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
const.class_eval { private_class_method *private_class_methods } if private_class_methods.present?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsWatcher
|
3
|
+
module DefaultInstanceHandler
|
4
|
+
def self.log call_stack_instacne
|
5
|
+
# save files in a new thread
|
6
|
+
# so that won't slow down the main thread
|
7
|
+
Thread.new do
|
8
|
+
@output_path ||= File.join(Rails.root, "tmp/rails_watcher")
|
9
|
+
now = Time.now
|
10
|
+
folder_name = "#{now.strftime("%Y%m%d_%H%M%S")}|#{call_stack_instacne.request_path.gsub("/", "_")}"
|
11
|
+
|
12
|
+
path = File.join(@output_path, folder_name)
|
13
|
+
FileUtils.mkdir_p path
|
14
|
+
|
15
|
+
%w[stack method_call_table db_query_table read_cache_table render_stack].each do |var|
|
16
|
+
filename = File.join path, "#{var}.json"
|
17
|
+
File.open(filename, 'w') do |f|
|
18
|
+
f.puts call_stack_instacne.instance_variable_get(:"@#{var}").to_json
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
summary_file = File.join path, "summary.json"
|
23
|
+
File.open(summary_file, 'w') do |f|
|
24
|
+
summary = {
|
25
|
+
duration: call_stack_instacne.duration,
|
26
|
+
request_path: call_stack_instacne.request_path,
|
27
|
+
logged_time: now
|
28
|
+
}
|
29
|
+
f.puts summary.to_json
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsWatcher
|
3
|
+
class Engine < ::Rails::Engine
|
4
|
+
isolate_namespace RailsWatcher
|
5
|
+
|
6
|
+
if ENV["RAILS_WATCHER"] == 'true'
|
7
|
+
|
8
|
+
initializer "rails_watcher.middleware" do |app|
|
9
|
+
app.config.app_middleware.use RailsWatcher::WatcherMiddleware
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "rails_watcher.modify_rails_methods" do
|
13
|
+
ActiveSupport.on_load(:action_controller) do
|
14
|
+
prepend Patches::ActionControllerRender
|
15
|
+
end
|
16
|
+
|
17
|
+
ActiveSupport.on_load(:action_view) do
|
18
|
+
prepend Patches::ActionViewRender
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
config.after_initialize do
|
23
|
+
RailsWatcher.my_watch_begins
|
24
|
+
|
25
|
+
%w[render_template.action_view render_partial.action_view render_collection.action_view].each do |event_type|
|
26
|
+
ActiveSupport::Notifications.subscribe(event_type) do |*args|
|
27
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
28
|
+
next unless call_stack
|
29
|
+
event = ActiveSupport::Notifications::Event.new *args
|
30
|
+
payload = event.payload
|
31
|
+
payload[:identifier] = payload[:identifier].sub(Rails.root.to_s, "")
|
32
|
+
call_stack.log_render_stack event_type, event.duration, payload
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
ActiveSupport::Notifications.subscribe('cache_read.active_support') do |*args|
|
37
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
38
|
+
next unless call_stack
|
39
|
+
event = ActiveSupport::Notifications::Event.new *args
|
40
|
+
|
41
|
+
call_stack.log_catche_read event.duration, event.payload
|
42
|
+
end
|
43
|
+
|
44
|
+
ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
|
45
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
46
|
+
next unless call_stack
|
47
|
+
event = ActiveSupport::Notifications::Event.new *args
|
48
|
+
|
49
|
+
call_stack.log_db_query event.duration, event.payload
|
50
|
+
end
|
51
|
+
|
52
|
+
RailsWatcher.configuration.rails_methods_i_want_to_watch.each do |component, instance_methods|
|
53
|
+
ActiveSupport.on_load(component) do
|
54
|
+
patch = Module.new do
|
55
|
+
instance_methods.each do |method_name|
|
56
|
+
define_method(method_name) do |*args, &blk|
|
57
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
58
|
+
return super(*args, &blk) unless call_stack
|
59
|
+
|
60
|
+
ret = nil
|
61
|
+
call_stack.log_method_call("#{component}##{method_name}") do
|
62
|
+
ret = super(*args, &blk)
|
63
|
+
end
|
64
|
+
ret
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
prepend patch
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsWatcher
|
3
|
+
module Patches
|
4
|
+
module KernelLoad
|
5
|
+
def load filename, wrap=false
|
6
|
+
ret = super
|
7
|
+
|
8
|
+
relative_path = begin
|
9
|
+
pn = Pathname.new filename
|
10
|
+
pn.relative_path_from(Rails.root).to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
if !RailsWatcher.configuration.ignored_files.include?(relative_path) &&
|
14
|
+
!RailsWatcher.configuration.ignored_paths.any? { |ignored_path| relative_path.start_with?(ignored_path) }
|
15
|
+
|
16
|
+
const_name = RailsWatcher.configuration.file_constant_mapping[relative_path] ||
|
17
|
+
RailsWatcher.guess_const_name(relative_path)
|
18
|
+
|
19
|
+
return ret if RailsWatcher.configuration.ignored_constants.include? const_name
|
20
|
+
|
21
|
+
if (Object.const_defined?(const_name) rescue return ret)
|
22
|
+
ConstModifier.modify const_name, filename
|
23
|
+
elsif Object.const_defined? const_name.upcase
|
24
|
+
ConstModifier.modify const_name.upcase, filename
|
25
|
+
else
|
26
|
+
warn <<~WARNING
|
27
|
+
============== Rails Watcher Warning ==============
|
28
|
+
Find constant failed:
|
29
|
+
File path: "#{relative_path}"
|
30
|
+
Guessed const name: "#{const_name}"
|
31
|
+
============== Rails Watcher Warning ==============
|
32
|
+
WARNING
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
ret
|
37
|
+
end
|
38
|
+
end # module KernelLoad
|
39
|
+
|
40
|
+
module ActionControllerRender
|
41
|
+
def render *args
|
42
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
43
|
+
if call_stack
|
44
|
+
ret = nil
|
45
|
+
method_tag = "render"
|
46
|
+
call_stack.log_method_call method_tag do
|
47
|
+
ret = super
|
48
|
+
end
|
49
|
+
ret
|
50
|
+
else
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module ActionViewRender
|
57
|
+
def render options = {}, locals = {}, &block
|
58
|
+
call_stack = RailsWatcher::CallStack.get_instance
|
59
|
+
if call_stack
|
60
|
+
ret = nil
|
61
|
+
method_tag = "render_partial (#{options.to_s[0, 70]})"
|
62
|
+
call_stack.log_method_call method_tag do
|
63
|
+
ret = super
|
64
|
+
end
|
65
|
+
ret
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def self.guess_const_name file_path
|
75
|
+
@@load_paths ||= (
|
76
|
+
Rails.application.config.autoload_paths +
|
77
|
+
Rails.application.config.eager_load_paths
|
78
|
+
).map do |autoload_path|
|
79
|
+
pn = Pathname.new autoload_path
|
80
|
+
pn.relative_path_from(Rails.root).to_s + "/"
|
81
|
+
end.to_set
|
82
|
+
|
83
|
+
@@load_paths.reduce(file_path) do |truncated_file_path, load_path|
|
84
|
+
truncated_file_path
|
85
|
+
.sub(/^#{load_path}/, "")
|
86
|
+
.sub(/^concerns/, "")
|
87
|
+
.sub(/\.rb$/, "")
|
88
|
+
end.camelize
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module RailsWatcher
|
3
|
+
class WatcherMiddleware
|
4
|
+
def initialize app
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call env
|
9
|
+
CallStack.set_instance env["REQUEST_PATH"]
|
10
|
+
status, headers, response = nil, nil, nil
|
11
|
+
duration = Benchmark.ms { status, headers, response = @app.call env }
|
12
|
+
CallStack.clear_instance_and_log duration
|
13
|
+
return status, headers, response
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
if ENV["RAILS_WATCHER"]
|
2
|
+
RailsWatcher.configuration do |config|
|
3
|
+
|
4
|
+
#
|
5
|
+
# ignored_files : files don't want to watch, default is `[]`
|
6
|
+
#
|
7
|
+
# config.ignored_files = ['app/articles_controller.rb']
|
8
|
+
#
|
9
|
+
|
10
|
+
|
11
|
+
#
|
12
|
+
# ignored_constants: Constants don't want to watch, default is `[]`
|
13
|
+
#
|
14
|
+
# config.ignored_constants = ['ArticlesController']
|
15
|
+
#
|
16
|
+
|
17
|
+
|
18
|
+
#
|
19
|
+
# ignored_paths: Paths don't want to watch, default is `[]`
|
20
|
+
#
|
21
|
+
# config.ignored_constants = ['vendor']
|
22
|
+
#
|
23
|
+
|
24
|
+
#
|
25
|
+
# ignored_methods: Methods don't want to watch, default is `{}`
|
26
|
+
#
|
27
|
+
# config.ignored_methods = {
|
28
|
+
# "UsersController" => {
|
29
|
+
# public: %i[method_1, method_2]
|
30
|
+
# },
|
31
|
+
# "WeirdMethods" => %i[some_method_in_any_file]
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
|
35
|
+
#
|
36
|
+
# ignored_request_path: Request pathes don't want to watch, default is `[]`
|
37
|
+
#
|
38
|
+
# config.ignored_request_path = []
|
39
|
+
#
|
40
|
+
|
41
|
+
|
42
|
+
#
|
43
|
+
# request_duration_threshold: don't watch request fast then `request_duration_threshold`ms, default is `10` (ms)
|
44
|
+
#
|
45
|
+
# config.request_duration_threshold = 10 # ms
|
46
|
+
#
|
47
|
+
|
48
|
+
#
|
49
|
+
# rails_methods_i_want_to_watch: rails methods want to watch, default is `{}`
|
50
|
+
# key: rails component, value: array of methods
|
51
|
+
#
|
52
|
+
# config.rails_methods_i_want_to_watch = {
|
53
|
+
# action_view: %i[
|
54
|
+
# javascript_include_tag
|
55
|
+
# stylesheet_link_tag
|
56
|
+
# javascript_tag
|
57
|
+
# content_for
|
58
|
+
# ]
|
59
|
+
# }
|
60
|
+
#
|
61
|
+
|
62
|
+
#
|
63
|
+
# instance_handler: how to handle result, default is `"RailsWatcher::DefaultInstanceHandler"`, see `rails_watcher/default_instance_handler.rb` for details.
|
64
|
+
#
|
65
|
+
# config.instance_handler = "RailsWatcher::DefaultInstanceHandler"
|
66
|
+
#
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
namespace :rails_watcher do
|
2
|
+
desc "Initialize rails watcher configuration file"
|
3
|
+
task :install do
|
4
|
+
template = File.join(__dir__, 'configuration.rb.tmp')
|
5
|
+
target_file = File.join(Rails.root, "config/initializers/rails_watcher_config.rb")
|
6
|
+
FileUtils.cp template, target_file
|
7
|
+
puts "Done: generated #{target_file}"
|
8
|
+
end
|
9
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_watcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- piecehealth
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-05-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5'
|
27
|
+
description: Help developers to understand, analyze your Rails application.
|
28
|
+
email:
|
29
|
+
- piecehealth@sina.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- MIT-LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- lib/rails_watcher.rb
|
38
|
+
- lib/rails_watcher/call_stack.rb
|
39
|
+
- lib/rails_watcher/configuration.rb
|
40
|
+
- lib/rails_watcher/const_modifier.rb
|
41
|
+
- lib/rails_watcher/default_instance_handler.rb
|
42
|
+
- lib/rails_watcher/engine.rb
|
43
|
+
- lib/rails_watcher/patches.rb
|
44
|
+
- lib/rails_watcher/version.rb
|
45
|
+
- lib/rails_watcher/watcher_middleware.rb
|
46
|
+
- lib/tasks/configuration.rb.tmp
|
47
|
+
- lib/tasks/rails_watcher_tasks.rake
|
48
|
+
homepage: https://github.com/piecehealth/rails_watcher
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.0.1
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: A profiling tool for Rails application.
|
71
|
+
test_files: []
|