├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── app └── application.rb ├── config.ru ├── lib └── sickbay │ ├── health_checker.rb │ └── service_health_checker.rb └── spec ├── integration ├── app │ └── routes_spec.rb └── lib │ └── sickbay │ └── health_checker_spec.rb ├── spec_helper.rb └── unit └── lib └── sickbay ├── health_checker_spec.rb └── service_health_checker_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.1 4 | script: bundle exec rspec 5 | addons: 6 | code_climate: 7 | repo_token: e6ed10751be318667895e2cdb34b1c70abfa46d7a2c79b3dfec1abf042a2330f 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | 5 | gem 'pry' 6 | 7 | group 'test' do 8 | gem 'rack-test' 9 | gem 'rspec' 10 | gem 'webmock' 11 | gem 'coveralls', require: false 12 | end 13 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.5.0) 5 | public_suffix (~> 2.0, >= 2.0.2) 6 | coderay (1.1.1) 7 | coveralls (0.8.15) 8 | json (>= 1.8, < 3) 9 | simplecov (~> 0.12.0) 10 | term-ansicolor (~> 1.3) 11 | thor (~> 0.19.1) 12 | tins (>= 1.6.0, < 2) 13 | crack (0.4.3) 14 | safe_yaml (~> 1.0.0) 15 | diff-lcs (1.2.5) 16 | docile (1.1.5) 17 | hashdiff (0.3.0) 18 | json (2.0.2) 19 | method_source (0.8.2) 20 | pry (0.10.4) 21 | coderay (~> 1.1.0) 22 | method_source (~> 0.8.1) 23 | slop (~> 3.4) 24 | public_suffix (2.0.4) 25 | rack (1.6.4) 26 | rack-protection (1.5.3) 27 | rack 28 | rack-test (0.6.3) 29 | rack (>= 1.0) 30 | rspec (3.5.0) 31 | rspec-core (~> 3.5.0) 32 | rspec-expectations (~> 3.5.0) 33 | rspec-mocks (~> 3.5.0) 34 | rspec-core (3.5.4) 35 | rspec-support (~> 3.5.0) 36 | rspec-expectations (3.5.0) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.5.0) 39 | rspec-mocks (3.5.0) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.5.0) 42 | rspec-support (3.5.0) 43 | safe_yaml (1.0.4) 44 | simplecov (0.12.0) 45 | docile (~> 1.1.0) 46 | json (>= 1.8, < 3) 47 | simplecov-html (~> 0.10.0) 48 | simplecov-html (0.10.0) 49 | sinatra (1.4.7) 50 | rack (~> 1.5) 51 | rack-protection (~> 1.4) 52 | tilt (>= 1.3, < 3) 53 | slop (3.6.0) 54 | term-ansicolor (1.4.0) 55 | tins (~> 1.0) 56 | thor (0.19.1) 57 | tilt (2.0.2) 58 | tins (1.13.0) 59 | webmock (2.1.0) 60 | addressable (>= 2.3.6) 61 | crack (>= 0.3.2) 62 | hashdiff 63 | 64 | PLATFORMS 65 | ruby 66 | 67 | DEPENDENCIES 68 | coveralls 69 | pry 70 | rack-test 71 | rspec 72 | sinatra 73 | webmock 74 | 75 | BUNDLED WITH 76 | 1.13.6 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 Igor Marques 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sickbay 2 | Get the HTTP status of a bunch of URLs in a single JSON response. Ideal for monitoring a lot of services at once. 3 | 4 | [![Code Climate](https://codeclimate.com/github/IgorMarques/sickbay/badges/gpa.svg)](https://codeclimate.com/github/IgorMarques/sickbay) 5 | [![Build Status](https://travis-ci.org/IgorMarques/sickbay.svg?branch=master)](https://travis-ci.org/IgorMarques/sickbay) 6 | [![Coverage Status](https://coveralls.io/repos/github/IgorMarques/sickbay/badge.svg?branch=master)](https://coveralls.io/github/IgorMarques/sickbay?branch=master) 7 | 8 | ```shell 9 | $ curl -X GET 'http://localhost:9292?google=http://www.google.com.br&bing=http://www.bing.com' 10 | ``` 11 | 12 | ```json 13 | { 14 | "google": "200", 15 | "bing": "200" 16 | } 17 | ``` 18 | 19 | ## Why? 20 | 21 | This app can be very useful for healthchecking multiple applications at the same time. 22 | 23 | The app is also easily deployable on heroku on any other service of your preference. 24 | 25 | One app that uses Sickbay for healthchecking is The Nurse [click here for more info](https://github.com/IgorMarques/The-Nurse). 26 | 27 | ## Live App 28 | 29 | You can check the app at https://sickbay.herokuapp.com 30 | 31 | Acessing: 32 | 33 | https://sickbay.herokuapp.com/?google=http://www.google.com&bing=http://www.bing.com 34 | 35 | For instance should return you the statuses of Google and Bing. 36 | 37 | 38 | ## Setup 39 | 40 | Assuming you have a proper Ruby workspace setted up, just run: 41 | 42 | ```shell 43 | $ git clone http://github.com/IgorMarques/sickbay 44 | $ cd sickbay 45 | $ bundle install 46 | ``` 47 | 48 | ## Running the Server 49 | 50 | Since this is a sinatra app, you just need to run: 51 | 52 | ```shell 53 | $ rackup 54 | ``` 55 | 56 | The output should be something similar to: 57 | 58 | ```shell 59 | [2016-11-22 20:10:13] INFO WEBrick 1.3.1 60 | [2016-11-22 20:10:13] INFO ruby 2.3.0 (2015-12-25) [x86_64-darwin16] 61 | [2016-11-22 20:10:13] INFO WEBrick::HTTPServer#start: pid=3389 port=9292 62 | ``` 63 | 64 | Simple as that :) 65 | 66 | ## Using the App 67 | 68 | Just send a `GET` request with the URLs you want to check as params. Each key of the param will be returned as a key of the response JSON: 69 | 70 | ```shell 71 | $ curl -X GET 'http://localhost:9292?google=http://www.google.com.br&bing=http://www.bing.com' 72 | ``` 73 | 74 | ```json 75 | { 76 | "google": "200", 77 | "bing": "200" 78 | } 79 | ``` 80 | 81 | Sending invalid or unreachable URLs will get a '0' as status: 82 | 83 | ```shell 84 | $ curl -X GET 'http://localhost:9292?some_bad_url=this_is_not_a_real_url' 85 | ``` 86 | ```json 87 | { 88 | "some_bad_url": "0" 89 | } 90 | ``` 91 | 92 | ## Testing 93 | 94 | This app uses [Rspec](https://github.com/rspec/rspec) for testing. To run the test suit: 95 | 96 | ```shell 97 | $ rspec 98 | ``` 99 | 100 | ## Contributing 101 | 102 | Just fork the app and send a pull-request! Improvements are welcome :) 103 | -------------------------------------------------------------------------------- /app/application.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'json' 3 | 4 | require_relative '../lib/sickbay/health_checker' 5 | 6 | get '/' do 7 | HealthChecker.call(params).to_json 8 | end 9 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './app/application.rb' 2 | 3 | run Sinatra::Application 4 | -------------------------------------------------------------------------------- /lib/sickbay/health_checker.rb: -------------------------------------------------------------------------------- 1 | require_relative 'service_health_checker' 2 | 3 | class HealthChecker 4 | def self.call(services = {}) 5 | services.each_with_object(Hash.new) do |(service_name, service_url), base_hash| 6 | base_hash[service_name] = ServiceHealthChecker.call(service_url) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/sickbay/service_health_checker.rb: -------------------------------------------------------------------------------- 1 | require 'uri' 2 | require 'net/http' 3 | 4 | class ServiceHealthChecker 5 | class << self 6 | def call(service_url) 7 | return '0' if service_url.nil? 8 | 9 | uri = generate_uri(service_url) 10 | 11 | Net::HTTP.get_response(uri).code 12 | rescue SocketError 13 | return '0' 14 | end 15 | 16 | private 17 | 18 | def generate_uri(url) 19 | if url.start_with?('http://') || url.start_with?('https://') 20 | return URI(url) 21 | else 22 | return URI('http://' + url) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/integration/app/routes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'webmock/rspec' 3 | 4 | describe Sinatra::Application, type: :controller do 5 | before do 6 | stub_request(:get, 'http://www.main_app.com/health') 7 | stub_request(:get, 'http://www.secondary_app.com/health') 8 | end 9 | 10 | context 'when getting a health check' do 11 | let(:response) { 12 | get '/', 13 | { 14 | main_service: 'http://www.main_app.com/health', 15 | secondary_app: 'http://www.secondary_app.com/health', 16 | } 17 | last_response 18 | } 19 | 20 | it 'responds with HTTP 200 OK' do 21 | expect(response.status).to eq(200) 22 | end 23 | 24 | it 'returns the health check for each service' do 25 | expect(response.body).to eq( 26 | { 27 | main_service: '200', 28 | secondary_app: '200' 29 | }.to_json 30 | ) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/integration/lib/sickbay/health_checker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'webmock/rspec' 3 | 4 | RSpec.describe HealthChecker do 5 | describe "#call" do 6 | before do 7 | stub_request(:get, 'http://www.service1.com') 8 | stub_request(:get, 'http://www.service2.com') 9 | .to_return(status: [500, "Internal Server Error"]) 10 | stub_request(:get, 'http://www.service3.com') 11 | end 12 | 13 | it 'gets the response status for each service' do 14 | expect(described_class.call( 15 | service_1: 'http://www.service1.com', 16 | service_2: 'http://www.service2.com', 17 | service_3: 'http://www.service3.com', 18 | service_4: nil 19 | )).to eq( 20 | service_1: '200', 21 | service_2: '500', 22 | service_3: '200', 23 | service_4: '0' 24 | ) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # spec/spec_helper.rb 2 | require 'rack/test' 3 | require 'rspec' 4 | require 'coveralls' 5 | 6 | ENV['RACK_ENV'] = 'test' 7 | 8 | Coveralls.wear! 9 | 10 | require File.expand_path '../../app/application.rb', __FILE__ 11 | 12 | module RSpecMixin 13 | include Rack::Test::Methods 14 | def app() Sinatra::Application end 15 | end 16 | 17 | RSpec.configure { |c| c.include RSpecMixin } 18 | -------------------------------------------------------------------------------- /spec/unit/lib/sickbay/health_checker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe HealthChecker do 4 | describe "#call" do 5 | before do 6 | allow(ServiceHealthChecker).to receive(:call).and_return('200') 7 | end 8 | 9 | it 'gets the response status for each service' do 10 | result = described_class.call( 11 | service_1: 'http://www.service1.com', 12 | service_2: 'http://www.service2.com', 13 | service_3: 'http://www.service3.com' 14 | ) 15 | 16 | expect(ServiceHealthChecker).to have_received(:call).with('http://www.service1.com') 17 | expect(ServiceHealthChecker).to have_received(:call).with('http://www.service2.com') 18 | expect(ServiceHealthChecker).to have_received(:call).with('http://www.service3.com') 19 | 20 | expect(result).to eq( 21 | service_1: '200', 22 | service_2: '200', 23 | service_3: '200' 24 | ) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/unit/lib/sickbay/service_health_checker_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'webmock/rspec' 3 | 4 | RSpec.describe ServiceHealthChecker do 5 | describe "#call" do 6 | context 'when param is a url' do 7 | before do 8 | stub_request(:get, 'http://www.example.com') 9 | stub_request(:get, 'https://www.example.com') 10 | end 11 | 12 | context 'and starts with http://' do 13 | it 'returns the response status for a get to the called url' do 14 | expect(described_class.call('http://www.example.com')).to eq('200') 15 | end 16 | end 17 | 18 | context 'and starts with https://' do 19 | it 'returns the response status for a get to the called url' do 20 | expect(described_class.call('https://www.example.com')).to eq('200') 21 | end 22 | end 23 | 24 | context 'but it does not start with http://' do 25 | it 'returns the response status for a get to the called url' do 26 | expect(described_class.call('www.example.com')).to eq('200') 27 | end 28 | end 29 | end 30 | 31 | context 'when the param is an unreachable url' do 32 | before do 33 | stub_request(:get, 'http://this_is_not_a_url').to_raise(SocketError) 34 | end 35 | 36 | it 'returns 0' do 37 | expect(described_class.call('this_is_not_a_url')).to eq('0') 38 | end 39 | end 40 | 41 | context 'when the param is nil' do 42 | it 'returns 0' do 43 | expect(described_class.call(nil)).to eq('0') 44 | end 45 | end 46 | end 47 | end 48 | --------------------------------------------------------------------------------