The smart way to check health of a Rails app

… and check why 5600+ Rails engineers read also this

The smart way to check health of a Rails app

Recently we added monitoring to one of our customer’s application. The app was tiny, but with a huge responsibility. We simply wanted to know if it’s alive. We went with Sensu HTTP check since it was a no-brainer. And it just worked, however, we got warning from monitoring tool.

This is not the HTTP code you are looking for

Authentication is required to access any of given app resources. It simply does redirect to login page. 302 code is returned instead of expected one from 2xx family.

That’s not what satisfies us.

What to do about that?

We’ve found out that the best solution would be having a dedicated endpoint in the app. This endpoint should be cheap for app server to respond. It shouldn’t require any authentication nor unexpected redirection. It should only return 204 No Content. Monitoring checks will be green and everyone will be happy.

Implementation

We decided to implement /health in our app. Nonetheless, we agreed that it’s a really good practice to do such checks in all of our apps and we released a tiny gem for that. Just to easily reuse this approach. The gem is named wet-healt_endpoint. Btw. We had to prefix health_endpoint with something since all simple names are already taken in the Rubygems world.

The gem consists of Middleware which is being attached close to the response in the app’s request-response cycle. It checks if application responds to such route, it not it responds to the client with 204 No Content. We used such approach not to override already existing endpoints in an app. Just in case, someone is developing app related to health.

module Wet
  module HealthEndpoint
    class Middleware
      def initialize(app)
        @app = app
      end

      def call(env)
        dup._call(env)
      end

      def _call(env)
        status, headers, body = @app.call(env)
        return [204, {}, ['']] if status == 404 &&
          env.fetch('PATH_INFO') == '/health'
        [status, headers, body]
      ensure
        body.close if body && body.respond_to?(:close) && $!
      end
    end
  end
end

That’s how it’s attached to the app:

require 'wet/health_endpoint/middleware'

module Wet
  module HealthEndpoint
    class Railtie < Rails::Railtie
      initializer 'health_endpoint.routes' do |app|
        app.middleware.use Middleware
      end
    end
  end
end

To use it, you simply need to add

gem 'wet-health_endpoint'

to your Gemfile and run bundle install.

How to check if it works

You can simply run a curl command

$ curl -I http://example.com/health
HTTP/1.1 204 No Content
Cache-Control: no-cache
X-Request-Id: 89d3c0c8-0b5c-421b-83a1-757dd04fef30
X-Runtime: 0.000578
Connection: close

or even better, write a test:

require 'test_helper'

class ApplicationHasHealthMonitoringEnabled < ActionDispatch::IntegrationTest

  def test_health_returns_204
    get "/health"
    assert_response(204)
  end
end

You can do even more!

Reverse proxies like Haproxy or Elastic Load Balancer understand if app instance is down and don’t route traffic to such ones.

Please see the sample Haproxy configuration:

backend my_fancy_app
  option httpcheck get /health
  http-check expect status 204
  default-server inter 3s fall 3 rise 2
  server srv1 10.0.0.1:80 check
  server srv2 10.0.0.2:80 check

Ok, so we order Haproxy to make a GET request to /health endpoint. We consider everything is ok if 204 code is returned. The action is performed every 3 seconds. After 3 sequential failures, an instance is marked as failed and no traffic is being sent there. After 2 successful checks instance is considered healthy. Last two lines specify which instances should be checked.

A sum up

It’s better to know that the app is down from your monitoring tool than from angry customer’s call. ;)

You might also like