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