From a7441ce180462bcce7ad6d5e71afbe21097a658e Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Tue, 4 Aug 2015 11:05:17 -0700 Subject: [PATCH] Instrument with ActiveSupport::Notifications --- .rubocop_todo.yml | 2 +- CHANGELOG.md | 1 + README.md | 30 +++++++++++++++ lib/grape.rb | 1 + lib/grape/endpoint.rb | 74 +++++++++++++++++++++---------------- spec/grape/endpoint_spec.rb | 62 +++++++++++++++++++++++++++++++ 6 files changed, 137 insertions(+), 33 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 4a12379922..5812142f9c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -16,7 +16,7 @@ Metrics/BlockNesting: # Offense count: 4 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 246 + Max: 252 # Offense count: 23 Metrics/CyclomaticComplexity: diff --git a/CHANGELOG.md b/CHANGELOG.md index e0ac9efa64..2a9004ccc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Next Release * [#1047](/~https://github.com/ruby-grape/grape/pull/1047): Adds `given` to DSL::Parameters, allowing for dependent params - [@rnubel](/~https://github.com/rnubel). * [#1064](/~https://github.com/ruby-grape/grape/pull/1064): Add public `Grape::Exception::ValidationErrors#full_messages` - [@romanlehnert](/~https://github.com/romanlehnert). * [#1079](/~https://github.com/ruby-grape/grape/pull/1079): Added `stream` method to take advantage of `Rack::Chunked` [@zbelzer](/~https://github.com/zbelzer). +* [#1086](/~https://github.com/ruby-grape/grape/pull/1086): Added ActiveSupport::Notifications instrumentation - [@wagenet](/~https://github.com/wagenet). * Your contribution here! #### Fixes diff --git a/README.md b/README.md index 05e4c29122..e34c5336e3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ - [Reloading in Rack Applications](#reloading-in-rack-applications) - [Reloading in Rails Applications](#reloading-in-rails-applications) - [Performance Monitoring](#performance-monitoring) + - [Active Support Instrumentation](#active-support-instrumentation) + - [Monitoring Products](#monitoring-products) - [Contributing to Grape](#contributing-to-grape) - [Hacking on Grape](#hacking-on-grape) - [License](#license) @@ -2628,6 +2630,34 @@ See [StackOverflow #3282655](http://stackoverflow.com/questions/3282655/ruby-on- ## Performance Monitoring +### Active Support Instrumentation + +Grape has built-in support for [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) which provides simple hook points to instrument key parts of your application. + +The following are currently supported: + +#### endpoint_run.grape + +The main execution of an endpoint, includes filters and rendering. + +* *endpoint* - The endpoint instance + +#### endpoint_render.grape + +The execution of the main content block of the endpoint. + +* *endpoint* - The endpoint instance + +#### endpoint_run_filters.grape + +* *endpoint* - The endpoint instance +* *filters* - The filters being executed +* *type* - The type of filters (before, before_validation, after_validation, after) + +See the [ActiveSupport::Notifications documentation](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html] for information on how to subscripe to these events. + +### Monitoring Products + Grape integrates with NewRelic via the [newrelic-grape](/~https://github.com/flyerhzm/newrelic-grape) gem, and with Librato Metrics with the [grape-librato](/~https://github.com/seanmoon/grape-librato) gem. diff --git a/lib/grape.rb b/lib/grape.rb index 32928ed48a..1148a9128f 100644 --- a/lib/grape.rb +++ b/lib/grape.rb @@ -15,6 +15,7 @@ require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/except' require 'active_support/dependencies/autoload' +require 'active_support/notifications' require 'multi_json' require 'multi_xml' require 'virtus' diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 3aa65e6d9b..5d19b8d7cb 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -41,10 +41,16 @@ def generate_api_method(method_name, &block) if instance_methods.include?(method_name.to_sym) || instance_methods.include?(method_name.to_s) fail NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") end + define_method(method_name, &block) method = instance_method(method_name) remove_method(method_name) - proc { |endpoint_instance| method.bind(endpoint_instance).call } + + proc do |endpoint_instance| + ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do + method.bind(endpoint_instance).call + end + end end end @@ -210,47 +216,49 @@ def equals?(e) protected def run(env) - @env = env - @header = {} + ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do + @env = env + @header = {} - @request = Grape::Request.new(env) - @params = @request.params - @headers = @request.headers + @request = Grape::Request.new(env) + @params = @request.params + @headers = @request.headers - cookies.read(@request) + cookies.read(@request) - self.class.before_each.call(self) if self.class.before_each + self.class.before_each.call(self) if self.class.before_each - run_filters befores + run_filters befores, :before - run_filters before_validations + run_filters before_validations, :before_validation - # Retrieve validations from this namespace and all parent namespaces. - validation_errors = [] + # Retrieve validations from this namespace and all parent namespaces. + validation_errors = [] - # require 'pry-byebug'; binding.pry + # require 'pry-byebug'; binding.pry - route_setting(:saved_validations).each do |validator| - begin - validator.validate!(params) - rescue Grape::Exceptions::Validation => e - validation_errors << e + route_setting(:saved_validations).each do |validator| + begin + validator.validate!(params) + rescue Grape::Exceptions::Validation => e + validation_errors << e + end end - end - if validation_errors.any? - fail Grape::Exceptions::ValidationErrors, errors: validation_errors, headers: header - end + if validation_errors.any? + fail Grape::Exceptions::ValidationErrors, errors: validation_errors, headers: header + end - run_filters after_validations + run_filters after_validations, :after_validation - response_object = @block ? @block.call(self) : nil - run_filters afters - cookies.write(header) + response_object = @block ? @block.call(self) : nil + run_filters afters, :after + cookies.write(header) - # The Body commonly is an Array of Strings, the application instance itself, or a File-like object. - response_object = file || [body || response_object] - [status, header, response_object] + # The Body commonly is an Array of Strings, the application instance itself, or a File-like object. + response_object = file || [body || response_object] + [status, header, response_object] + end end def build_middleware @@ -305,9 +313,11 @@ def helpers mod end - def run_filters(filters) - (filters || []).each do |filter| - instance_eval(&filter) + def run_filters(filters, type = :other) + ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do + (filters || []).each do |filter| + instance_eval(&filter) + end end end diff --git a/spec/grape/endpoint_spec.rb b/spec/grape/endpoint_spec.rb index 19593c4b75..c4d87e262d 100644 --- a/spec/grape/endpoint_spec.rb +++ b/spec/grape/endpoint_spec.rb @@ -962,4 +962,66 @@ def memoized expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*') end end + + context 'instrumentation' do + before do + subject.before do + # Placeholder + end + subject.get do + 'hello' + end + + @events = [] + @subscriber = ActiveSupport::Notifications.subscribe(/grape/) do |*args| + @events << ActiveSupport::Notifications::Event.new(*args) + end + end + + after do + ActiveSupport::Notifications.unsubscribe(@subscriber) + end + + it 'notifies AS::N' do + get '/' + + # In order that the events finalized (time each block ended) + expect(@events).to contain_exactly( + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: a_collection_containing_exactly(an_instance_of(Proc)), + type: :before }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: [], + type: :before_validation }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: [], + type: :after_validation }), + have_attributes(name: 'endpoint_render.grape', payload: { endpoint: an_instance_of(Grape::Endpoint) }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: [], + type: :after }), + have_attributes(name: 'endpoint_run.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + env: an_instance_of(Hash) }) + ) + + # In order that events were initialized + expect(@events.sort_by(&:time)).to contain_exactly( + have_attributes(name: 'endpoint_run.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + env: an_instance_of(Hash) }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: a_collection_containing_exactly(an_instance_of(Proc)), + type: :before }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: [], + type: :before_validation }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: [], + type: :after_validation }), + have_attributes(name: 'endpoint_render.grape', payload: { endpoint: an_instance_of(Grape::Endpoint) }), + have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: an_instance_of(Grape::Endpoint), + filters: [], + type: :after }) + ) + end + end end