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).
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
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With