New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experimental - DO NOT MERGE! Auto-instrumentation of Controller Time #198
Conversation
No rush, but I tried adding this to an app, but I'm not seeing any auto-instrumentation. Relevant diffs below:
|
Also:
I noticed that |
This helps address scoutapp/roadmap#68 |
Correct - needs Ruby 2.3.1+ currently. |
Okay, I've made a first stab at improving this. I've added some tests. I suggest that the next step is to ensure we are generating the correct instrumentation and add a handful more tests. |
The code right now is very much Rails specific, but I've left it open for the future, we can probably modify it to suit different frameworks e.g. Sinatra. |
ff8917f
to
b254fdc
Compare
Do you have any prescribed method for doing integration tests? i.e. create a default rails site, insert the I feel like this would be a nice addition. |
Gemfile
Outdated
@@ -10,3 +10,7 @@ if RUBY_VERSION <= "1.8.7" | |||
gem "pry", "~> 0.9.12" | |||
gem "rake", "~> 10.5" | |||
end | |||
|
|||
group :development do | |||
gem "parser" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be a dev-time gemspec dependency instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, no problem.
7122fb7
to
d834dfb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ioquatix awesome! A few things I want to go over and that need fixing. I'll ping you so we can chat about them
if path =~ CONTROLLER_PATH_PATTERN | ||
new_code = Rails.rewrite(path) | ||
|
||
return compile(new_code, path, filepath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filepath
is undefined. Also, I think the arguments to compile
are wrong - https://ruby-doc.org/core-2.5.0/RubyVM/InstructionSequence.html#method-c-compile
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, my bad, we should probably add some tests for this.
module ScoutApm | ||
module AutoInstrument | ||
module InstructionSequence | ||
CONTROLLER_PATH_PATTERN = /\/apm\/app\/controllers\/.*_controller.rb$/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to make sure we remove the \/apm
portion here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
@@ -0,0 +1,73 @@ | |||
|
|||
# In order for this to work, you must add `gem 'parser'` to your Gemfile. | |||
require 'parser/current' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking we'll be more defensive here and use conditionals around our definitions and code in order to prevent things blowing up if someone doesn't include parser
or we're running in an older ruby version, non MRI, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I wrapped it and rescue LoadError
. Let's see how this works on travis.
column = node.location.column || 'column?' | ||
method_name = node.children[1] || '*unknown*' | ||
|
||
wrap(node.location.expression, "::ScoutApm::Instruments::AutoInstruments.dynamic_layer('#{method_name}:l#{line}:c#{column}'){", "}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
::ScoutApm::Instruments::AutoInstruments.dynamic_layer
si so... ugly 😜 Thinking about how we can make that better. We mentioned about the invisible space or hot dog. Need to decide on options here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use ::ScoutApm::AutoInstrument{...}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented this change in recent commit.
layer = ScoutApm::Layer.new('AutoInstrument', name) | ||
req.start_layer(layer) | ||
started_layer = true | ||
puts "================ STARTED AUTO INSTRUMENT LAYER #{name} =============" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reminder to remove this debug code before releasing to customers
test/unit/auto_instrument_test.rb
Outdated
def test_rails_controller_rewrite | ||
assert_equal instrumented_source("controller"), ::ScoutApm::AutoInstrument::Rails.rewrite(source_path("controller")) | ||
end | ||
end if RUBY_VERSION >= "2.3.1" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can test the rewriter on all versions. It's the load_iseq
feature that's version dependent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot a crucial bug in super
for load_iseq
|
||
return compile(new_code, path, filepath) | ||
else | ||
return super |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot this crucial one: we must assume super
(load_iseq
) does not exist (but it might exist).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I reworked it to be compile_file
. It's simple solution for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably write super rescue compile_file(path)
but that might be a bad idea, because it could eat up other exceptions.
db4b355
to
16d7443
Compare
- There is no `super` implementation, so invoke `compile_file`. - The 2nd argument of `compile` should be a file name.
- Only match user-defined controllers in the Rails app directory. - Revert to original source code if instrumentation fails.
7adfbb8
to
b10d6ec
Compare
end | ||
|
||
def instrument(method_name, line, column) | ||
["::ScoutApm::AutoInstrument(\"\#{self.class}\\\##{method_name}:L#{line}:C#{column}\"){", "}"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The desc
field of a layer may make sense to put an exact line & column number in.
So the name can be Foo/length
and desc is line 3, column 8
or whatever format you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The latest version of this puts the class and controller method with line number into the name field and the first line of code/expression into the description field. How do you feel about that? Can you try it and see if it makes sense?
4a2bc0a
to
14d1c5b
Compare
When instrumenting specific lines of code in the controller, we aren't failing the entire request if that line fails, because the controller may catch the error and do something with it (e.g. error page).
Thanks for the work on this! Just one finding. On Heroku, the Rails console is not run using bundler, so requiring in
|
Should we set the version to something like |
Replaced by #247 |
Install instructions:
rubocop
to the Gemfilerequire 'scout_apm/auto_instrument'
toconfig/boot.rb