Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get response status after every controller is executed in Ruby on Rails

I have a RoR Rest API and I want to emit a metric with the status of each response of my API. I managed to do that for all the cases except from those where the controller is crashing.

For example with the following code in the ApplicationController:

require 'statsd-ruby'

class ApplicationController < ActionController::API
  after_action :push_status_metric

  def push_status_metric
    statsd = Statsd.new ENV['STATSD_LOCATION'], ENV['STATSD_PORT']
    puts normalize_status_metric(response.status)

    statsd.increment('ds.status.' + normalize_status_metric(response.status).to_s + '.int') unless request.fullpath == '/health'
  end

  private

  def normalize_status_metric(status)
    return 100 if status >= 100 && status < 200
    return 200 if status >= 200 && status < 300
    return 300 if status >= 300 && status < 400
    return 400 if status >= 400 && status < 500
    return 500 if status >= 500 && status < 600
    0
  end
end

But this solution doesn't capture errors such as ActionController::RoutingError and ActiveRecord::RecordNotFound.

I tried the following code:

  rescue_from StandardError do |exception|
    statsd = Statsd.new ENV['STATSD_LOCATION'], ENV['STATSD_PORT']
    statsd.increment('ds.status.' + normalize_status_metric(response.status).to_s + '.int') unless request.fullpath == '/health'

    raise exception
  end

But when this callback is executed, the response.status value is always 200 (seems like it's not set yet by the framework up to this point).

like image 385
Panos Avatar asked Mar 16 '18 08:03

Panos


1 Answers

Knowing the the rails logger manages to do this, we can take a look at its class, ActionController::LogSubscriber and that process_action method. So, the status in that event could be nil, and we can see how they then convert an exception to a status, if an exception exists:

status = payload[:status]
if status.nil? && payload[:exception].present?
  exception_class_name = payload[:exception].first
  status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
end

So now, we can do something similar by subscribing to this event on our own, with Active Support Instrumentation, by creating an initializer:

ActiveSupport::Notifications.subscribe 'process_action.action_controller' do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)

  # opening a file here is probably a performance nightmare, but you'd be doing something with statsd, not a file, anyway
  open('metrics.txt', 'a') do |f|
    # get the action status, this code is from the ActionController::LogSubscriber
    status = event.payload[:status]

    if status.nil? && event.payload[:exception].present?
      exception_class_name = event.payload[:exception].first

      status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
    end

    f.puts "process_action.action_controller controller: #{event.payload[:controller]} - action: #{event.payload[:action]} - path: #{event.payload[:path]} - status: #{status}"
  end
end

and hitting it a few times with a simple controller:

class HomeController < ApplicationController
  def non_standard_status
    render html: 'This is not fine', status: :forbidden
  end

  def error
    raise ActiveRecord::RecordNotFound, 'No Records Found'
  end

  def another_error
    raise ArgumentError, 'Some Argument is wrong'
  end

  def this_is_fine
    render html: 'This is fine'
  end
end

yields a file:

process_action.action_controller controller: HomeController - action: non_standard_status - path: /forbidden - status: 403
process_action.action_controller controller: HomeController - action: error - path: /error - status: 404
process_action.action_controller controller: HomeController - action: another_error - path: /error2 - status: 500
process_action.action_controller controller: HomeController - action: this_is_fine - path: /fine - status: 200
like image 89
Simple Lime Avatar answered Sep 20 '22 15:09

Simple Lime