├── .github ├── dependabot.yml └── workflows │ ├── release-please.yml │ └── tests.yml ├── .gitignore ├── .release-please-manifest.json ├── .rspec ├── .rubocop.yml ├── .tool-versions ├── .yamllint ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── app └── controllers │ └── rapporteur │ ├── application_controller.rb │ └── statuses_controller.rb ├── bin ├── console └── setup ├── config.ru ├── config ├── locales │ └── en.yml └── routes.rb ├── gemfiles ├── rails7.1.gemfile ├── rails7.gemfile ├── rails8.0.gemfile ├── rails_latest.gemfile ├── sinatra2.x.gemfile ├── sinatra3.x.gemfile ├── sinatra4.x.gemfile └── sinatra_latest.gemfile ├── lib ├── rapporteur.rb └── rapporteur │ ├── check_list.rb │ ├── checker.rb │ ├── checker_deprecations.rb │ ├── checks.rb │ ├── checks │ └── active_record_check.rb │ ├── engine.rb │ ├── message_list.rb │ ├── revision.rb │ ├── rspec.rb │ ├── rspec3.rb │ └── version.rb ├── rapporteur.gemspec ├── release-please-config.json └── spec ├── internal ├── config │ ├── database.yml │ └── routes.rb ├── db │ └── schema.rb ├── log │ └── .gitignore └── public │ └── favicon.ico ├── models ├── check_list_spec.rb └── message_list_spec.rb ├── requests ├── active_record_check_spec.rb ├── halt_spec.rb ├── messsage_addition_spec.rb ├── no_checks_spec.rb ├── revision_check_spec.rb ├── sinatra_spec.rb └── time_check_spec.rb ├── route_helper.rb ├── routing └── routes_spec.rb └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | updates: 3 | - allow: 4 | - dependency-type: "direct" 5 | - dependency-type: "indirect" 6 | directory: "/" 7 | open-pull-requests-limit: 10 8 | package-ecosystem: "bundler" 9 | schedule: 10 | interval: "daily" 11 | versioning-strategy: "increase-if-necessary" 12 | - directory: "/" 13 | package-ecosystem: "github-actions" 14 | schedule: 15 | interval: "daily" 16 | version: 2 17 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | --- 2 | jobs: 3 | release-please: 4 | outputs: 5 | release_created: "${{ steps.release.outputs.release_created }}" 6 | tag_name: "${{ steps.release.outputs.tag_name }}" 7 | version_number: "${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}" 8 | permissions: 9 | contents: "write" 10 | issues: "write" 11 | pull-requests: "write" 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - id: "release" 15 | uses: "googleapis/release-please-action@v4" 16 | 17 | release: 18 | if: "${{ needs.release-please.outputs.release_created }}" 19 | name: "Release" 20 | needs: 21 | - "release-please" 22 | permissions: 23 | id-token: "write" 24 | contents: "write" 25 | runs-on: "ubuntu-latest" 26 | steps: 27 | - uses: "actions/checkout@v4" 28 | with: 29 | fetch-tags: true 30 | - uses: "ruby/setup-ruby@v1" 31 | with: 32 | bundler-cache: true 33 | - uses: "rubygems/release-gem@v1" 34 | 35 | name: "Release" 36 | 37 | "on": 38 | push: 39 | branches: 40 | - "main" 41 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | jobs: 3 | tests: 4 | env: 5 | BUNDLE_GEMFILE: "${{matrix.gemfile}}" 6 | name: "Tests" 7 | runs-on: "ubuntu-latest" 8 | steps: 9 | - uses: "actions/checkout@v4" 10 | - uses: "ruby/setup-ruby@v1" 11 | with: 12 | bundler-cache: true 13 | - name: "Run Tests" 14 | run: "bundle exec rake spec" 15 | - name: "Check Ruby Style" 16 | run: "bundle exec rubocop -c ./.rubocop.yml -fq" 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | gemfile: 21 | - "Gemfile" 22 | - "gemfiles/rails_latest.gemfile" 23 | - "gemfiles/rails7.1.gemfile" 24 | - "gemfiles/rails7.gemfile" 25 | - "gemfiles/rails8.0.gemfile" 26 | - "gemfiles/sinatra_latest.gemfile" 27 | - "gemfiles/sinatra2.x.gemfile" 28 | - "gemfiles/sinatra3.x.gemfile" 29 | - "gemfiles/sinatra4.x.gemfile" 30 | name: "Tests" 31 | "on": 32 | pull_request: 33 | branches: 34 | - "main" 35 | push: 36 | branches: 37 | - "main" 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /gemfiles/*.lock 5 | /gemfiles/.bundle/ 6 | /_yardoc/ 7 | /coverage/ 8 | /doc/ 9 | /pkg/ 10 | /spec/reports/ 11 | /tmp/ 12 | 13 | # rspec failure tracking 14 | .rspec_status 15 | 16 | *.sqlite 17 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "3.9.0" 3 | } 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | --require spec_helper 4 | --warnings 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | inherit_gem: 4 | rubocop-rails_config: 5 | - "config/rails.yml" 6 | 7 | AllCops: 8 | Exclude: 9 | - 'gemfiles/*' 10 | - 'gemfiles/**/*' 11 | - 'vendor/**/*' 12 | TargetRubyVersion: 2.5 13 | 14 | Layout/CaseIndentation: 15 | EnforcedStyle: "end" 16 | 17 | Layout/EmptyLinesAroundAccessModifier: 18 | EnforcedStyle: "around" 19 | 20 | Layout/EndOfLine: 21 | EnforcedStyle: "lf" 22 | 23 | Layout/IndentationConsistency: 24 | EnforcedStyle: "normal" 25 | 26 | Style/StringLiterals: 27 | EnforcedStyle: "single_quotes" 28 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.4.2 2 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | key-ordering: "enable" 7 | quoted-strings: "enable" 8 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise 'rails7.1' do 4 | gem 'rails', '~> 7.1.0' 5 | end 6 | 7 | appraise 'rails7' do 8 | gem 'rails', '~> 7.1' 9 | end 10 | 11 | appraise 'rails8.0' do 12 | gem 'rails', '~> 8.0.0' 13 | end 14 | 15 | appraise 'rails-latest' do 16 | gem 'rails' 17 | end 18 | 19 | appraise 'sinatra2.x' do 20 | gem 'sinatra', '~> 2.0' 21 | end 22 | 23 | appraise 'sinatra3.x' do 24 | gem 'sinatra', '~> 3.0' 25 | end 26 | 27 | appraise 'sinatra4.x' do 28 | gem 'sinatra', '~> 4.0' 29 | end 30 | 31 | appraise 'sinatra-latest' do 32 | gem 'sinatra' 33 | end 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Rapporteur changelog 2 | 3 | [![Gem Version](https://badge.fury.io/rb/rapporteur.svg)](https://badge.fury.io/rb/rapporteur) 4 | [![Tests](https://github.com/envylabs/rapporteur/actions/workflows/tests.yml/badge.svg)](https://github.com/envylabs/rapporteur/actions/workflows/tests.yml) 5 | 6 | ## [3.9.0](https://github.com/envylabs/rapporteur/compare/v3.8.0...v3.9.0) (2025-04-19) 7 | 8 | 9 | ### Features 10 | 11 | * set minimum Ruby version to 3.2 ([58ce959](https://github.com/envylabs/rapporteur/commit/58ce959804714e0a2e51e5610e4816c7b7d7eb9f)) 12 | 13 | ## [HEAD][] / unreleased 14 | 15 | * Remove testing of Rails 5.2 as it is no longer officially maintained. 16 | 17 | ## [3.8.0][] / 2022-01-23 18 | 19 | * Add support for Rails 7.0. 20 | 21 | ## [3.7.2][] / 2021-06-22 22 | 23 | * Test support for Rails 6.1. 24 | 25 | ## [3.7.1][] / 2020-09-25 26 | 27 | * Fix a Ruby 2.7 keyword argument deprecation warning in MessageList. 28 | 29 | ## [3.7.0][] / 2020-01-02 30 | 31 | * Add and test support for Rails 6.0. This also drops explicit testing of Rails 32 | 5.0 and 5.1 as they are no longer officially maintained. 33 | 34 | ## [3.6.4][] / 2019-04-04 35 | 36 | * Maintenance release, no functional changes. 37 | 38 | ## [3.6.3][] / 2018-10-03 39 | 40 | * Ensure the `Rapporteur::ApplicationController` is loaded and utilized. 41 | 42 | ## [3.6.2][] / 2018-09-11 43 | 44 | * Allow i18n 1.x versions as a dependency. 45 | 46 | ## [3.6.1][] / 2018-05-20 47 | 48 | * Convert Checker#messages and #errors into Thread-local variables. See 49 | [#17](https://github.com/envylabs/rapporteur/issues/17) and 50 | [#18](https://github.com/envylabs/rapporteur/issues/18), thanks to 51 | [nevinera][]. 52 | 53 | ## [3.6.0][] / 2017-06-10 54 | 55 | * Add `expires_now` to the Rails controller's status response. This ensures the 56 | `Cache-Control` header instructs the client to not cache the response and 57 | avoids the `ETag` header being generated. 58 | 59 | ## [3.5.1][] / 2016-02-08 60 | 61 | * Fix automatic Rapporteur mount detection logic under Rails 4.0 and 4.1. The 62 | mapper constraints used in 4.2 were not used in 4.0 and 4.1, causing a method 63 | reference error. See [#15](https://github.com/envylabs/rapporteur/issues/15), 64 | thanks to [sshaw][]. 65 | 66 | ## [3.5.0][] / 2016-01-28 67 | 68 | * Add a backward-compatible fallback to auto-mount the `Rapporteur::Engine` in 69 | a Rails application and deprecation warning if it is not explicitly mounted 70 | by the application. 71 | * Require the parent application to explicitly mount the `Rapporteur::Engine` 72 | in their Rails application. This adds mount point flexibility at the cost of 73 | configuration. 74 | * Change ActiveRecord to use a more database agnostic `select_value` query to 75 | determine availability. See 76 | [#12](https://github.com/envylabs/rapporteur/issues/12). 77 | 78 | ## [3.4.0][] / 2016-01-06 79 | 80 | * Update the route generation code to no longer use a `.routes` method that 81 | becomes private in Rails 5. Thanks to [lsylvester][]. 82 | 83 | ## [3.3.0][] / 2015-02-03 84 | 85 | * Remove the customized Rapporteur::Responder (an ActionController::Responder) 86 | since responders were removed from Rails core in version 4.2 and inline the 87 | logic into the StatusesController. 88 | * Register Rapporteur::Engine routes for Rails 4.2 support. 89 | * Auto-mount the engine routes and force definition of an application `status` 90 | route for backward compatibility. Otherwise, developers would seemingly need 91 | to either manually mount the engine or define an application-level named 92 | route for the status endpoint. 93 | 94 | ## [3.2.0][] / 2015-02-02 95 | 96 | * Update the Rails route definition to force (and default) a JSON format. The 97 | intent is to fix an issue where Rails auto-appended a `(.:format)` segment to 98 | the fixed route and broke `/status.json` route matching. See 99 | [#9](https://github.com/envylabs/rapporteur/issues/9). 100 | 101 | ## [3.1.0][] / 2014-07-03 102 | 103 | * Remove the explicit railties dependency (was at `'>= 3.1', '< 4.2'`). This 104 | allows Rapporteur to be used in Sinatra applications without loading 105 | railties, actionpack, etc. 106 | * Update the packaged RSpec matchers to allow matching against regular 107 | expressions or strings. 108 | * Add support for deprecation-less RSpec 3 by introducing rapporteur/rspec3. 109 | * Removed official support for Rails 3.1.x, as it is no longer supported by the 110 | Rails core team. 111 | 112 | ## [3.0.2][] / 2014-05-17 113 | 114 | * Test for and allow compatibility with railties 4.1. 115 | 116 | ## [3.0.1][] / 2013-08-23 117 | 118 | * Add back missing support for I18n interpolated values which was lost with the 119 | customized message lists. 120 | 121 | ## [3.0.0][] / 2013-08-23 122 | 123 | * Fix/add Ruby 1.8 compatibility. Because this library was built to work with 124 | Rails 3.1 and 3.2 (as well as 4.0), not supporting Ruby 1.8 was dishonest. 125 | That has now been rectified. 126 | * Update message and error handling to allow for both I18n/Proc/String support 127 | for both types of messages, by replacing ActiveModel::Errors with a local 128 | message list. This provides better consistency between the error message and 129 | success message implementations. 130 | * Use a customized check list registry to ensure order persistence and object 131 | uniqueness across Ruby versions. 132 | * Upgrade Combustion development support gem to fix deprecations in Rails 4.0. 133 | 134 | ### :boom: Backward incompatible changes 135 | 136 | * Attributes may now have multiple success messages bound to them for 137 | reporting. This means that there are now, possibly, more than one message per 138 | key. 139 | 140 | ## [2.1.0][] / 2013-06-28 141 | 142 | * Update the gemspec to allow for Rails 4.0 environments. 143 | * Update the packaged Responder to properly handle Rails 4.0. 144 | * Fix deprecation notice for Checker.clear. 145 | 146 | ## [2.0.1][] / 2013-05-31 147 | 148 | * Fix NoMethodError in CheckerDeprecations#clear. 149 | 150 | ## [2.0.0][] / 2013-05-31 151 | 152 | * Removed active_model_serializers dependency. 153 | * Extracted time and revision checks into Checks::TimeCheck and 154 | Checks::RevisionCheck, and applied them as the default checks. 155 | * Updated Checker#add_error to allow for I18n interpolated values. 156 | * Updated Checker#add_check to take a block in addition to a lambda or object 157 | that responds to #call. 158 | * Added Checker#halt! which checks can call to short-circuit processing of any 159 | further checks. 160 | 161 | ### :boom: Backward incompatible changes 162 | 163 | * Flattened the messages key in the JSON response. All messages are now 164 | included at the top level of the hash. 165 | * It's now possible to remove all checks by calling Checker#clear. This 166 | includes the default TimeCheck and RevisionCheck checks. 167 | * Simplified the I18n scope to "rapporteur.errors.{attribute}.{key}". This 168 | means that Checker#add_error now takes at least 2 arguments, similarly to 169 | ActiveModel::Errors#add. 170 | * Added a facade for all Checker interaction. Clients should not use 171 | Rapporteur::Checker.add_check et al, in favor of using Rapporteur.add_check. 172 | 173 | ## [1.1.0][] / 2013-05-30 174 | 175 | * Add the ability to define custom successful response messages via 176 | add_message. This allows Check authors to relay automated information forward 177 | to other external systems. 178 | 179 | ## [1.0.1][] / 2013-05-20 180 | 181 | * Improve the gemspec's minimum runtime- and development-dependency version 182 | accuracies. 183 | 184 | ## 1.0.0 / 2013-05-19 185 | 186 | * Initial public release. 187 | 188 | 189 | [lsylvester]: https://github.com/lsylvester 190 | [nevinera]: https://github.com/nevinera 191 | [sshaw]: https://github.com/sshaw 192 | 193 | [1.0.1]: https://github.com/envylabs/rapporteur/compare/v1.0.0...v1.0.1 194 | [1.1.0]: https://github.com/envylabs/rapporteur/compare/v1.0.1...v1.1.0 195 | [2.0.0]: https://github.com/envylabs/rapporteur/compare/v1.1.0...v2.0.0 196 | [2.0.1]: https://github.com/envylabs/rapporteur/compare/v2.0.0...v2.0.1 197 | [2.1.0]: https://github.com/envylabs/rapporteur/compare/v2.0.1...v2.1.0 198 | [3.0.0]: https://github.com/envylabs/rapporteur/compare/v2.1.0...v3.0.0 199 | [3.0.1]: https://github.com/envylabs/rapporteur/compare/v3.0.0...v3.0.1 200 | [3.0.2]: https://github.com/envylabs/rapporteur/compare/v3.0.1...v3.0.2 201 | [3.1.0]: https://github.com/envylabs/rapporteur/compare/v3.0.2...v3.1.0 202 | [3.2.0]: https://github.com/envylabs/rapporteur/compare/v3.1.0...v3.2.0 203 | [3.3.0]: https://github.com/envylabs/rapporteur/compare/v3.2.0...v3.3.0 204 | [3.4.0]: https://github.com/envylabs/rapporteur/compare/v3.3.0...v3.4.0 205 | [3.5.0]: https://github.com/envylabs/rapporteur/compare/v3.4.0...v3.5.0 206 | [3.5.1]: https://github.com/envylabs/rapporteur/compare/v3.5.0...v3.5.1 207 | [3.6.0]: https://github.com/envylabs/rapporteur/compare/v3.5.1...v3.6.0 208 | [3.6.1]: https://github.com/envylabs/rapporteur/compare/v3.6.0...v3.6.1 209 | [3.6.2]: https://github.com/envylabs/rapporteur/compare/v3.6.1...v3.6.2 210 | [3.6.3]: https://github.com/envylabs/rapporteur/compare/v3.6.2...v3.6.3 211 | [3.6.4]: https://github.com/envylabs/rapporteur/compare/v3.6.3...v3.6.4 212 | [3.7.0]: https://github.com/envylabs/rapporteur/compare/v3.6.4...v3.7.0 213 | [3.7.1]: https://github.com/envylabs/rapporteur/compare/v3.7.0...v3.7.1 214 | [3.7.2]: https://github.com/envylabs/rapporteur/compare/v3.7.1...v3.7.2 215 | [3.8.0]: https://github.com/envylabs/rapporteur/compare/v3.7.2...v3.8.0 216 | [HEAD]: https://github.com/envylabs/rapporteur/compare/v3.8.0...main 217 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | 6 | gem 'activerecord-jdbcsqlite3-adapter', platforms: :jruby 7 | gem 'rails', '>= 7.1', '< 8.1' 8 | gem 'sqlite3', platforms: :ruby 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Envy Labs LLC and Code School LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rapporteur (rap-or-TUHR) 2 | 3 | [![Gem Version](https://badge.fury.io/rb/rapporteur.svg)](https://badge.fury.io/rb/rapporteur) 4 | [![Tests](https://github.com/envylabs/rapporteur/actions/workflows/tests.yml/badge.svg)](https://github.com/envylabs/rapporteur/actions/workflows/tests.yml) 5 | 6 | 7 | This gem provides a singular, status-checking endpoint to your application. The 8 | endpoint provides a JSON response with either an HTTP 200 or an HTTP 500 9 | response, depending on the current application environment. 10 | 11 | When the environment tests successfully, an HTTP 200 response is returned with 12 | the current application Git revision and server time: 13 | 14 | ```json 15 | { 16 | "revision": "906731e6467ea381ba5bc70f103b85ed4178fee7", 17 | "time": "2013-05-19T05:38:46Z" 18 | } 19 | ``` 20 | 21 | When an application validation fails, an HTTP 500 response is returned with a 22 | collection of error messages, similar to the Rails < 4.2 responders for model 23 | validations: 24 | 25 | ```json 26 | { 27 | "errors": { 28 | "database": ["The application database is inaccessible or unavailable"] 29 | } 30 | } 31 | ``` 32 | 33 | ## Installation 34 | 35 | To install, add this line to your application's Gemfile: 36 | 37 | ```ruby 38 | gem 'rapporteur' 39 | ``` 40 | 41 | And then execute: 42 | 43 | ```bash 44 | $ bundle install 45 | ``` 46 | 47 | ### Supported environments 48 | 49 | This library follows the maintenance policy of 50 | [Ruby](https://www.ruby-lang.org/en/downloads/branches/), 51 | [Ruby on Rails](https://guides.rubyonrails.org/maintenance_policy.html), and 52 | [Sinatra](https://github.com/sinatra/sinatra/blob/master/MAINTENANCE.md) for 53 | testing and maintaining these environments. 54 | 55 | Unsupported versions of Ruby, Ruby on Rails, or Sinatra may also work with this 56 | library, however they are not officially supported or tested against. 57 | 58 | ## Usage 59 | 60 | By default, there are no application checks that run and the status endpoint 61 | simply reports the current application revision and time. This is useful for a 62 | basic connectivity check to be watched by a third party service like Pingdom 63 | for a very simple, non-critical application. 64 | 65 | You may optionally use any of the pre-defined checks (such as the ActiveRecord 66 | connection check) to expand the robustness of the status checks. Adding a check 67 | will execute that check each time the status endpoint is requested, so be 68 | somewhat wary of doing _too_ much. See more in the [Adding checks 69 | section](#adding-checks), below. 70 | 71 | Further, you can define your own checks which could be custom to your 72 | application or environment and report their own, unique errors. The only 73 | requirement is that the check objects are callable (respond to `#call`, like a 74 | Proc). See more in the [Creating custom checks 75 | section](#creating-custom-checks), below. 76 | 77 | ### Usage in a Rails application 78 | 79 | If you're running in a Rails environment, then the included Rails Engine should 80 | automatically get required and loaded. However, you will need to instruct your 81 | application where to mount the status endpoint. In your `config/routes.rb`, add 82 | a `mount` at the endpoint you desire: 83 | 84 | ```ruby 85 | Rails.application.routes.draw do 86 | mount Rapporteur::Engine, at: '/status' 87 | end 88 | ``` 89 | 90 | This will mount the Rapporteur status endpoint at `/status` where it will 91 | listen for and respond to JSON requests (`/status.json`, for example). 92 | 93 | ### Usage in a Rack (non-Rails) application 94 | 95 | If you're running in a non-Rails, Rack environment (e.g. Sinatra), you can 96 | `require "rapporteur"` and use the status generator directly from your own 97 | endpoints. 98 | 99 | So, here is an example usage in Sinatra: 100 | 101 | ```ruby 102 | require 'rapporteur' 103 | require 'sinatra/base' 104 | 105 | class MyApp < Sinatra::Base 106 | get '/status.json' do 107 | content_type :json 108 | status(result.errors.empty? ? 200 : 500) 109 | body Rapporteur.run.as_json.to_json 110 | end 111 | end 112 | ``` 113 | 114 | Require `rapporteur` and then, at some point, call `Rapporteur.run` to execute 115 | the configured checks. `as_json` will convert the result of the `run` into a 116 | Hash which is ready for JSON generation. At that point, do whatever you need to 117 | do with the Hash to have your framework of choice generate and respond with 118 | JSON. 119 | 120 | ## Customization 121 | 122 | ### Adding checks 123 | 124 | This gem ships with the following checks tested and packaged: 125 | 126 | * **Rapporteur::Checks::ActiveRecordCheck** - Performs a trivial test 127 | of the current `ActiveRecord::Base.connection` to ensure basic database 128 | connectivity. 129 | 130 | To add checks to your application, define the checks you'd like to run in your 131 | environment or application configuration files or initializers, such as: 132 | 133 | ```ruby 134 | # config/initializers/rapporteur.rb 135 | Rapporteur.add_check(Rapporteur::Checks::ActiveRecordCheck) 136 | ``` 137 | 138 | Or, make an environment specific check with: 139 | 140 | ```ruby 141 | # config/environments/production.rb 142 | MyApplication.configure do 143 | config.to_prepare do 144 | Rapporteur.add_check(Rapporteur::Checks::ActiveRecordCheck) 145 | end 146 | end 147 | ``` 148 | 149 | ### Creating custom checks 150 | 151 | It is simple to add a custom check to the status endpoint. All that is required 152 | is that you give the checker an object that is callable. In your object, simply 153 | check for the state of the world that you're interested in, and if you're not 154 | happy with it, add an error to the given `checker` instance: 155 | 156 | ```ruby 157 | # config/initializers/rapporteur.rb 158 | 159 | # Define a simple check as a block: 160 | Rapporteur.add_check do |checker| 161 | checker.add_message(:paid, 'too much') 162 | end 163 | 164 | # Make and use a reusable Proc or lambda: 165 | my_proc_check = lambda { |checker| 166 | checker.add_error(:luck, :bad) if rand(2) > 0 167 | checker.add_message(:luck, :good) 168 | } 169 | Rapporteur.add_check(my_proc_check) 170 | 171 | # Package a check into a Class: 172 | class MyClassCheck 173 | def self.call(checker) 174 | @@counter ||= 0 175 | checker.add_error(:count, :exceeded) if @@counter > 50 176 | end 177 | end 178 | Rapporteur.add_check(MyClassCheck) 179 | ``` 180 | 181 | Certainly, the definition and registration of the checks do not need to occur 182 | within the same file, but you get the idea. Also: Please make your checks more 183 | useful than those defined above. ;) 184 | 185 | You could create a checker for your active Redis connection, Memcached 186 | connections, disk usage percentage, process count, memory usage, or really 187 | anything you like. Again, because these checks get executed every time the 188 | status endpoint is called, **be mindful of the tradeoffs when making a check that 189 | may be resource intensive**. 190 | 191 | ### Customizing the revision 192 | 193 | If you need to customize the way in which the current application revision is 194 | calculated (by default it runs a `git rev-parse HEAD`), you may do so by 195 | modifying the necessary environment file or creating an initializer in your 196 | Rails application: 197 | 198 | ```ruby 199 | # config/initializers/rapporteur.rb 200 | Rapporteur::Revision.current = 'revision123' 201 | ``` 202 | 203 | ```ruby 204 | # config/environments/production.rb 205 | MyApplication.configure do 206 | config.to_prepare do 207 | Rapporteur::Revision.current = 'revision123' 208 | end 209 | end 210 | ``` 211 | 212 | You may pass a String or a callable object (Proc) to `.current=` and it will be 213 | executed and memoized. Useful examples of this are: 214 | 215 | ```ruby 216 | # Read a Capistrano REVISION file 217 | Rapporteur::Revision.current = Rails.root.join('REVISION').read.strip 218 | 219 | # Force a particular directory and use Git 220 | Rapporteur::Revision.current = `cd "#{Rails.root}" && git rev-parse HEAD`.strip 221 | 222 | # Use an ENV variable (Heroku Bamboo Stack) 223 | Rapporteur::Revision.current = ENV['REVISION'] 224 | 225 | # Do some crazy calculation 226 | Rapporteur::Revision.current = lambda { MyRevisionCalculator.execute! } 227 | ``` 228 | 229 | ### Customizing the messages 230 | 231 | The success and error messages displayed in the event that application 232 | validations pass or fail are all optionally passed through I18n. There are 233 | default localization strings provided with the gem, but you may override them 234 | as necessary, by simply redefining the proper locale keys in a locale file 235 | within your local application. 236 | 237 | For example, to override the database check failure message: 238 | 239 | ```yaml 240 | # /config/locales/en.yml 241 | en: 242 | rapporteur: 243 | errors: 244 | database: 245 | unavailable: "Something went wrong" 246 | ``` 247 | 248 | If you created a custom checker which reports the current sky color, for 249 | example, and wanted the success messages to be localized, you could do the 250 | following: 251 | 252 | ```ruby 253 | # /config/initializers/rapporteur.rb 254 | sky_check = lambda { |checker| checker.add_message(:sky, :blue) } 255 | Rapporteur.add_check(sky_check) 256 | ``` 257 | 258 | ```yaml 259 | # /config/locales/fr.yml 260 | fr: 261 | rapporteur: 262 | messages: 263 | sky: 264 | blue: "bleu" 265 | ``` 266 | 267 | ## Contributing 268 | 269 | 1. Fork it 270 | 2. Create your feature branch (`git checkout -b my-new-feature`) 271 | 3. Commit your changes (`git commit -am 'Add some feature'`) 272 | 4. Push to the branch (`git push origin my-new-feature`) 273 | 5. Create new Pull Request 274 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /app/controllers/rapporteur/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | class ApplicationController < ActionController::Base 5 | protect_from_forgery with: :exception 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/rapporteur/statuses_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'application_controller' 4 | 5 | module Rapporteur 6 | class StatusesController < ApplicationController 7 | def show 8 | expires_now 9 | respond_to do |format| 10 | format.json do 11 | resource = Rapporteur.run 12 | 13 | if resource.errors.empty? 14 | render(json: resource) 15 | else 16 | display_errors(resource, :json) 17 | end 18 | end 19 | end 20 | end 21 | 22 | private 23 | 24 | def display_errors(resource, format) 25 | render(format => { errors: resource.errors }, :status => :internal_server_error) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'rapporteur' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | 6 | Bundler.require :default, :development 7 | 8 | Combustion.initialize! 9 | run Combustion::Application 10 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | en: 4 | rapporteur: 5 | errors: 6 | database: 7 | unavailable: "The application database is inaccessible or unavailable" 8 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rapporteur::Engine.routes.draw do 4 | get '/(.:format)', to: 'statuses#show', as: :status 5 | end 6 | 7 | explicitly_mounted = Rails.application.routes.routes.any? { |r| 8 | (Rapporteur::Engine == r.app) || (r.app.respond_to?(:app) && Rapporteur::Engine == r.app.app) 9 | } 10 | 11 | unless explicitly_mounted 12 | ActiveSupport::Deprecation.warn('Rapporteur was not explicitly mounted in your application. Please add an explicit mount call to your /config/routes.rb. Automatically mounted Rapporteur::Engine to /status for backward compatibility. This will be no longer automatically mount in Rapporteur 4.') # rubocop:disable Layout/LineLength 13 | Rails.application.routes.draw do 14 | mount Rapporteur::Engine, at: '/status' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /gemfiles/rails7.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails", "~> 7.1.0" 7 | gem "sqlite3", platforms: :ruby 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails7.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails", "~> 7.1" 7 | gem "sqlite3", platforms: :ruby 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails8.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails", "~> 8.0.0" 7 | gem "sqlite3", platforms: :ruby 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_latest.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails" 7 | gem "sqlite3", platforms: :ruby 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/sinatra2.x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails", ">= 7.1", "< 8.1" 7 | gem "sqlite3", platforms: :ruby 8 | gem "sinatra", "~> 2.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/sinatra3.x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails", ">= 7.1", "< 8.1" 7 | gem "sqlite3", platforms: :ruby 8 | gem "sinatra", "~> 3.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/sinatra4.x.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails", ">= 7.1", "< 8.1" 7 | gem "sqlite3", platforms: :ruby 8 | gem "sinatra", "~> 4.0" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/sinatra_latest.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord-jdbcsqlite3-adapter", platforms: :jruby 6 | gem "rails", ">= 7.1", "< 8.1" 7 | gem "sqlite3", platforms: :ruby 8 | gem "sinatra" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /lib/rapporteur.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rapporteur/engine' if defined?(Rails) 4 | require 'rapporteur/version' 5 | 6 | # Rapporteur is a Rails Engine which provides your application with an 7 | # application status endpoint. 8 | # 9 | module Rapporteur 10 | autoload :CheckList, 'rapporteur/check_list' 11 | autoload :Checker, 'rapporteur/checker' 12 | autoload :CheckerDeprecations, 'rapporteur/checker_deprecations' 13 | autoload :Checks, 'rapporteur/checks' 14 | autoload :MessageList, 'rapporteur/message_list' 15 | autoload :Revision, 'rapporteur/revision' 16 | 17 | # Public: Add a pre-built or custom check to your status endpoint. These 18 | # checks are used to test the state of the world of the application, and 19 | # need only respond to `#call`. 20 | # 21 | # Once added, the given check will be called and passed an instance of this 22 | # checker. If everything is good, do nothing! If there is a problem, use 23 | # `add_error` to add an error message to the checker. 24 | # 25 | # Examples 26 | # 27 | # Rapporteur.add_check { |checker| 28 | # checker.add_error("Bad luck.") if rand(2) == 1 29 | # } 30 | # 31 | # Returns the Checker instance. 32 | # Raises ArgumentError if the given check does not respond to call. 33 | # 34 | def self.add_check(object_or_nil_with_block = nil, &block) 35 | checker.add_check(object_or_nil_with_block, &block) 36 | end 37 | 38 | # Internal: The Checker instance. All toplevel calls on Rapporteur are 39 | # delgated to this object. 40 | # 41 | def self.checker 42 | unless @checker 43 | @checker = Checker.new 44 | add_check(Checks::RevisionCheck) 45 | add_check(Checks::TimeCheck) 46 | end 47 | @checker 48 | end 49 | @checker = nil 50 | 51 | # Public: Empties all configured checks from the checker. This may be 52 | # useful for testing and for cases where you might've built up some basic 53 | # checks but for one reason or another (environment constraint) need to 54 | # start from scratch. 55 | # 56 | # Returns the Checker instance. 57 | # 58 | def self.clear_checks 59 | checker.clear 60 | end 61 | 62 | # Public: This is the primary execution point for this class. Use run to 63 | # exercise the configured checker and collect any application errors or 64 | # data for rendering. 65 | # 66 | # Returns the Checker instance. 67 | # 68 | def self.run 69 | checker.run 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/rapporteur/check_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | # Manages a list of checks. 5 | # 6 | # The goals of this object are to store and return the check objects given to 7 | # it in the same order they were given (in Ruby 1.8 and newer). And, to 8 | # ensure that the same check is not added twice to be run. 9 | # 10 | # Previously, a native Ruby Set was used. However, Sets do not guarantee 11 | # order, particularly in Ruby 1.8. A simple Array is possible, but loses the 12 | # uniqueness constraint of the objects added. 13 | # 14 | class CheckList 15 | # Public: Returns a new, empty CheckList instance. 16 | # 17 | def initialize 18 | @list = [] 19 | end 20 | 21 | # Public: Add a new check to the list. 22 | # 23 | # Returns the CheckList instance. 24 | # 25 | def add(check) 26 | @list << check unless @list.include?(check) 27 | self 28 | end 29 | 30 | # Public: Empties all checks from the list. This functionally resets the 31 | # list to an initial state. 32 | # 33 | # Returns the CheckList instance. 34 | # 35 | def clear 36 | @list.clear 37 | self 38 | end 39 | 40 | # Public: Iterates over all of the contained objects and yields them out, 41 | # individually. 42 | # 43 | # Returns the CheckList instance. 44 | # 45 | def each(&block) 46 | @list.each(&block) 47 | self 48 | end 49 | 50 | # Public: Returns true if the list is empty. 51 | # 52 | def empty? 53 | @list.empty? 54 | end 55 | 56 | # Public: Returns the objects in the list in an Array. 57 | # 58 | def to_a 59 | @list.dup 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/rapporteur/checker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | # The center of the Rapporteur library, Checker manages holding and running 5 | # the custom checks, holding any application error messages, and provides the 6 | # controller with that data for rendering. 7 | # 8 | class Checker 9 | extend CheckerDeprecations 10 | 11 | def initialize 12 | @check_list = CheckList.new 13 | reset 14 | end 15 | 16 | # Public: Add a pre-built or custom check to your status endpoint. These 17 | # checks are used to test the state of the world of the application, and 18 | # need only respond to `#call`. 19 | # 20 | # Once added, the given check will be called and passed an instance of this 21 | # checker. If everything is good, do nothing! If there is a problem, use 22 | # `add_error` to add an error message to the checker. 23 | # 24 | # Examples 25 | # 26 | # Rapporteur.add_check { |checker| 27 | # checker.add_error(:luck, :bad) if rand(2) == 1 28 | # } 29 | # 30 | # Returns self. 31 | # Raises ArgumentError if the given check does not respond to call. 32 | # 33 | def add_check(object_or_nil_with_block = nil, &block) 34 | if block_given? 35 | check_list.add(block) 36 | elsif object_or_nil_with_block.respond_to?(:call) 37 | check_list.add(object_or_nil_with_block) 38 | else 39 | raise ArgumentError, 'A check must respond to #call.' 40 | end 41 | self 42 | end 43 | 44 | # Public: Empties all configured checks from the checker. This may be 45 | # useful for testing and for cases where you might've built up some basic 46 | # checks but for one reason or another (environment constraint) need to 47 | # start from scratch. 48 | # 49 | # Returns self. 50 | # 51 | def clear 52 | check_list.clear 53 | self 54 | end 55 | 56 | ## 57 | # Public: Checks can call this method to halt any further processing. This 58 | # is useful for critical or fatal check failures. 59 | # 60 | # For example, if load is too high on a machine you may not want to run any 61 | # other checks. 62 | # 63 | # Returns true. 64 | # 65 | def halt! 66 | @halted = true 67 | end 68 | 69 | # Public: This is the primary execution point for this class. Use run to 70 | # exercise the configured checker and collect any application errors or 71 | # data for rendering. 72 | # 73 | # Returns self. 74 | # 75 | def run 76 | reset 77 | check_list.each do |object| 78 | object.call(self) 79 | break if @halted 80 | end 81 | self 82 | end 83 | 84 | # Public: Add an error message to the checker in order to have it rendered 85 | # in the status request. 86 | # 87 | # It is suggested that you use I18n and locale files for these messages, as 88 | # is done with the pre-built checks. If you're using I18n, you'll need to 89 | # define `rapporteur.errors..`. 90 | # 91 | # name - A Symbol which indicates the attribute, name, or other identifier 92 | # for the check being run. 93 | # message - A String/Symbol/Proc to identify the message to provide in a 94 | # check failure situation. 95 | # 96 | # A String is presented as given. 97 | # A Symbol will attempt to translate through I18n or default to the String-form of the symbol. 98 | # A Proc which will be called and the return value will be presented as returned. 99 | # i18n_options - A Hash of options (likely variable key/value pairs) to 100 | # pass to I18n during translation. 101 | # 102 | # Examples 103 | # 104 | # checker.add_error(:you, "failed.") 105 | # checker.add_error(:using, :i18n_key_is_better) 106 | # checker.add_error(:using, :i18n_with_variable, :custom_variable => 'pizza') 107 | # 108 | # en: 109 | # rapporteur: 110 | # errors: 111 | # using: 112 | # i18n_key_is_better: 'Look, localization!' 113 | # i18n_with_variable: 'Look, %{custom_variable}!' 114 | # 115 | # Returns self. 116 | # 117 | def add_error(name, message, i18n_options = {}) 118 | errors.add(name, message, i18n_options) 119 | self 120 | end 121 | 122 | ## 123 | # Public: Adds a status message for inclusion in the success response. 124 | # 125 | # name - A String containing the name or identifier for your message. This 126 | # is unique and may be overriden by other checks using the name 127 | # message name key. 128 | # 129 | # message - A String/Symbol/Proc containing the message to present to the 130 | # user in a successful check sitaution. 131 | # 132 | # A String is presented as given. 133 | # A Symbol will attempt to translate through I18n or default to the String-form of the symbol. 134 | # A Proc which will be called and the return value will be presented as returned. 135 | # 136 | # Examples 137 | # 138 | # checker.add_message(:repository, 'git@github.com/user/repo.git') 139 | # checker.add_message(:load, 0.934) 140 | # checker.add_message(:load, :too_high, :measurement => '0.934') 141 | # 142 | # Returns self. 143 | # 144 | def add_message(name, message, i18n_options = {}) 145 | messages.add(name, message, i18n_options) 146 | self 147 | end 148 | 149 | ## 150 | # Internal: Returns a hash of messages suitable for conversion into JSON. 151 | # 152 | def as_json(_args = {}) 153 | messages.to_hash 154 | end 155 | 156 | ## 157 | # Internal: Used by Rails' JSON serialization to render error messages. 158 | # 159 | def errors 160 | Thread.current[:rapporteur_errors] ||= MessageList.new(:errors) 161 | end 162 | 163 | ## 164 | # Internal: Used by Rails' JSON serialization. 165 | # 166 | def read_attribute_for_serialization(key) 167 | messages[key] 168 | end 169 | 170 | alias read_attribute_for_validation read_attribute_for_serialization 171 | 172 | private 173 | 174 | attr_reader :check_list 175 | 176 | def messages 177 | Thread.current[:rapporteur_messages] ||= MessageList.new(:messages) 178 | end 179 | 180 | def reset 181 | @halted = false 182 | messages.clear 183 | errors.clear 184 | end 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/rapporteur/checker_deprecations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/deprecation' 4 | 5 | module Rapporteur 6 | module CheckerDeprecations 7 | def add_check(*args, &block) 8 | ActiveSupport::Deprecation.warn('use Rapporteur.add_check', caller) 9 | Rapporteur.add_check(*args, &block) 10 | end 11 | 12 | def clear 13 | ActiveSupport::Deprecation.warn('use Rapporteur.clear_checks', caller) 14 | Rapporteur.clear_checks 15 | end 16 | 17 | def run 18 | ActiveSupport::Deprecation.warn('use Rapporteur.run', caller) 19 | Rapporteur.run 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/rapporteur/checks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | module Checks 5 | autoload :ActiveRecordCheck, 'rapporteur/checks/active_record_check' 6 | 7 | # A check which simply reports the current clock time in UTC. This check is 8 | # useful because it shows that the status end point is not being cached and 9 | # allows you to determine if your server clocks are abnormally skewed. 10 | # 11 | # This check has no failure cases. 12 | # 13 | # Examples 14 | # 15 | # { 16 | # time: "2013-06-21T05:18:59Z" 17 | # } 18 | # 19 | TimeCheck = ->(checker) { checker.add_message(:time, Time.now.utc) } 20 | 21 | # A check which reports the current revision of the running application. 22 | # 23 | # This check has no failure cases. 24 | # 25 | # Examples 26 | # 27 | # { 28 | # revision: "c74edd04f64b25ff6691308bcfdefcee149aa4b5" 29 | # } 30 | # 31 | RevisionCheck = ->(checker) { checker.add_message(:revision, Revision.current) } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/rapporteur/checks/active_record_check.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | module Checks 5 | class ActiveRecordCheck 6 | def self.call(checker) 7 | ActiveRecord::Base.connection.select_value('SELECT current_time AS time') 8 | rescue StandardError 9 | checker.add_error(:database, :unavailable) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/rapporteur/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | class Engine < Rails::Engine 5 | isolate_namespace Rapporteur 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/rapporteur/message_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | require 'set' 5 | 6 | module Rapporteur 7 | # A container of keys and one or more messages per key. This acts similarly 8 | # to an ActiveModel::Errors collection from Ruby on Rails. 9 | # 10 | class MessageList 11 | extend Forwardable 12 | 13 | def_delegator :@messages, :clear 14 | def_delegator :@messages, :empty? 15 | 16 | # Public: Initialize a new MessageList instance. 17 | # 18 | # list_type - A Symbol representing the type of this list. This Symbol is 19 | # used to determine the structure of the I18n key used when 20 | # translating the given message Symbols. 21 | # Allowed values: :messages or :errors. 22 | # 23 | # Returns a MessageList instance. 24 | # 25 | def initialize(list_type) 26 | @list_type = list_type 27 | @messages = Hash.new { |hash, key| hash[key] = Set.new } 28 | end 29 | 30 | # Public: Adds a new message to the list for the given attribute. 31 | # 32 | # attribute - A Symbol describing the category, attribute, or name to which 33 | # the message pertains. 34 | # message - A Symbol, String, or Proc which contains the message for the 35 | # attribute. 36 | # 37 | # If a String is given, it is directly used for the message. 38 | # 39 | # If a Symbol is given, it is used to lookup the full message 40 | # from I18n, under the "rapporteur.." 41 | # key. For instance, if this is a :messages list, and the 42 | # attribute is :time, then I18n would be queried for 43 | # "rapporteur.messages.time". 44 | # 45 | # If a Proc is given, it is `.call`'d with no parameters. The 46 | # return value from the Proc is directly used for the message. 47 | # i18n_options - A Hash of optional key/value pairs to pass to the I18n 48 | # translator. 49 | # 50 | # Examples 51 | # 52 | # list.add(:time, '2013-08-23T12:34:00Z') 53 | # list.add(:time, :too_late) 54 | # list.add(:time, :too_late, :time => 'midnight') 55 | # list.add(:time, lambda { Time.now.iso8601 }) 56 | # 57 | # Returns the MessageList instance. 58 | # 59 | def add(attribute, message, i18n_options = {}) 60 | @messages[attribute.to_sym].add(normalize_message(attribute, message, i18n_options)) 61 | self 62 | end 63 | 64 | # Public: Generates an Array containing the combination of all of the added 65 | # attributes and their messages. 66 | # 67 | # Examples 68 | # 69 | # list.add(:time, 'is now') 70 | # list.add(:time, 'is valuable') 71 | # list.add(:day, 'is today') 72 | # list.full_messages 73 | # # => ["time is now", "time is valuable", "day is today"] 74 | # 75 | # Returns an Array containing Strings of messages. 76 | # 77 | def full_messages 78 | @messages.map { |attribute, attribute_messages| 79 | attribute_messages.map { |message| "#{attribute} #{message}" } 80 | }.flatten 81 | end 82 | 83 | # Public: Returns the added attributes and their messages as a Hash, keyed 84 | # by the attribute, with either an Array containing all of the added 85 | # messages or just the single attribute message. 86 | # 87 | # Examples 88 | # 89 | # list.add(:time, 'is now') 90 | # list.add(:time, 'is valuable') 91 | # list.add(:day, 'is today') 92 | # list.full_messages 93 | # # => {:time => ["is now", "is valuable"], :day => "is today"} 94 | # 95 | # Returns a Hash instance. 96 | # 97 | def to_hash 98 | hash = {} 99 | @messages.each_pair do |key, value| 100 | hash[key] = if value.size == 1 101 | value.first 102 | else 103 | value.to_a 104 | end 105 | end 106 | hash 107 | end 108 | 109 | private 110 | 111 | def generate_message(key, type, i18n_options) 112 | I18n.translate(type, **i18n_options.merge(default: [type, type.to_s], scope: [:rapporteur, @list_type, key])) 113 | end 114 | 115 | def normalize_message(attribute, message, i18n_options) 116 | case message 117 | when Symbol 118 | generate_message(attribute, message, i18n_options) 119 | when Proc 120 | message.call 121 | else 122 | message 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/rapporteur/revision.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | # Manages memoizing and maintaining the current application revision. 5 | # 6 | class Revision 7 | class_attribute :_current 8 | 9 | # Public: Returns the current revision as a String. 10 | # 11 | def self.current 12 | self._current ||= calculate_current 13 | end 14 | 15 | # Public: Forcibly sets the current application revision. 16 | # 17 | # revision - Either a String or a callable object (Proc, for example) to 18 | # use your own environment logic to determine the revision. 19 | # 20 | # Examples 21 | # 22 | # Rapporteur::Revision.current = ENV['REVISION'].strip 23 | # Rapporteur::Revision.current = Rails.root.join("REVISION").read.strip 24 | # 25 | # Returns the revision given. 26 | # 27 | def self.current=(revision) 28 | self._current = calculate_current(revision) 29 | end 30 | 31 | # Internal: The default method of determining the current revision. This 32 | # assumes a git executable is in the current PATH and that the process 33 | # context is running the the appropriate git application directory. 34 | # 35 | # Returns a String containing the current git revision, hopefully. 36 | # 37 | def self.default_revision_source 38 | `git rev-parse HEAD 2>/dev/null`.strip 39 | rescue StandardError 40 | end 41 | 42 | # Internal: Calculates the current revision from the configured revision 43 | # source. 44 | # 45 | def self.calculate_current(revision = default_revision_source) 46 | case revision 47 | when String 48 | revision 49 | when Proc 50 | revision.call.to_s 51 | when NilClass 52 | 'You must provide a Rapporteur::Revision.current= String or Proc' 53 | else 54 | raise ArgumentError, "Unknown revision type given: #{revision.inspect}" 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/rapporteur/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rapporteur' 4 | 5 | shared_examples_for 'a successful status response' do 6 | let(:parsed_body) { JSON.parse(response.body) } 7 | 8 | it 'responds with HTTP 200' do 9 | expect(response.response_code).to(eq(200)) 10 | end 11 | 12 | context 'the response headers' do 13 | it 'contains a Content-Type JSON header' do 14 | expect(response.media_type).to(eq(Mime[:json])) 15 | end 16 | 17 | it 'contains a Cache-Control header which disables client caching' do 18 | expect(response.headers.fetch('Cache-Control')).to include('no-cache') 19 | end 20 | end 21 | 22 | context 'the response payload' do 23 | it 'does not contain errors' do 24 | expect(parsed_body).not_to(have_key('errors')) 25 | end 26 | end 27 | end 28 | 29 | shared_examples_for 'an erred status response' do 30 | let(:parsed_body) { JSON.parse(response.body) } 31 | 32 | it 'responds with HTTP 500' do 33 | expect(response.response_code).to(eq(500)) 34 | end 35 | 36 | context 'the response headers' do 37 | it 'contains a Content-Type JSON header' do 38 | expect(response.media_type).to(eq(Mime[:json])) 39 | end 40 | 41 | it 'contains a Cache-Control header which disables client caching' do 42 | expect(response.headers.fetch('Cache-Control')).to include('no-cache') 43 | end 44 | end 45 | 46 | context 'the response payload' do 47 | it 'contains errors' do 48 | expect(parsed_body).to(have_key('errors')) 49 | expect(parsed_body.fetch('errors')).not_to(be_empty) 50 | end 51 | end 52 | end 53 | 54 | RSpec::Matchers.define :include_status_error_message do |attribute, message| 55 | match do |response| 56 | @body = JSON.parse(response.body) 57 | @body.fetch('errors', {}).fetch(attribute.to_s).match(message) 58 | end 59 | 60 | failure_message_for_should do |_actual| 61 | "expected #{@body.inspect} to include a #{attribute}:#{message.inspect} error message" 62 | end 63 | 64 | failure_message_for_should_not do |_actual| 65 | "expected #{@body.inspect} to not include a #{attribute}:#{message.inspect} error message" 66 | end 67 | end 68 | 69 | RSpec::Matchers.define :include_status_message do |name, message| 70 | match do |response| 71 | @body = JSON.parse(response.body) 72 | @body.has_key?(name) && @body.fetch(name).match(message) 73 | end 74 | 75 | failure_message_for_should do |_actual| 76 | "expected #{@body.inspect} to include a #{name.inspect}: #{message.inspect} message" 77 | end 78 | 79 | failure_message_for_should_not do |_actual| 80 | "expected #{@body.inspect} to not include a #{name.inspect}: #{message.inspect} message" 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/rapporteur/rspec3.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rapporteur' 4 | 5 | shared_examples_for 'a successful status response' do 6 | let(:parsed_body) { JSON.parse(response.body) } 7 | 8 | it 'responds with HTTP 200' do 9 | expect(response.response_code).to(eq(200)) 10 | end 11 | 12 | context 'the response headers' do 13 | it 'contains a Content-Type JSON header' do 14 | expect(response.media_type).to(eq(Mime[:json])) 15 | end 16 | 17 | it 'contains a Cache-Control header which disables client caching' do 18 | expect(response.headers.fetch('Cache-Control')).to include('no-cache') 19 | end 20 | end 21 | 22 | context 'the response payload' do 23 | it 'does not contain errors' do 24 | expect(parsed_body).not_to(have_key('errors')) 25 | end 26 | end 27 | end 28 | 29 | shared_examples_for 'an erred status response' do 30 | let(:parsed_body) { JSON.parse(response.body) } 31 | 32 | it 'responds with HTTP 500' do 33 | expect(response.response_code).to(eq(500)) 34 | end 35 | 36 | context 'the response headers' do 37 | it 'contains a Content-Type JSON header' do 38 | expect(response.media_type).to(eq(Mime[:json])) 39 | end 40 | 41 | it 'contains a Cache-Control header which disables client caching' do 42 | expect(response.headers.fetch('Cache-Control')).to include('no-cache') 43 | end 44 | end 45 | 46 | context 'the response payload' do 47 | it 'contains errors' do 48 | expect(parsed_body).to(have_key('errors')) 49 | expect(parsed_body.fetch('errors')).not_to(be_empty) 50 | end 51 | end 52 | end 53 | 54 | RSpec::Matchers.define :include_status_error_message do |attribute, message| 55 | match do |response| 56 | @body = JSON.parse(response.body) 57 | @body.fetch('errors', {}).fetch(attribute.to_s).match(message) 58 | end 59 | 60 | failure_message_when_negated do |_actual| 61 | "expected #{@body.inspect} to not include a #{attribute}:#{message.inspect} error message" 62 | end 63 | end 64 | 65 | RSpec::Matchers.define :include_status_message do |name, message| 66 | match do |response| 67 | @body = JSON.parse(response.body) 68 | 69 | @body.has_key?(name) && @body.fetch(name).match(message) 70 | end 71 | 72 | failure_message do |_actual| 73 | "expected #{@body.inspect} to include a #{name.inspect}: #{message.inspect} message" 74 | end 75 | 76 | failure_message_when_negated do |_actual| 77 | "expected #{@body.inspect} to not include a #{name.inspect}: #{message.inspect} message" 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/rapporteur/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rapporteur 4 | VERSION = '3.9.0' 5 | end 6 | -------------------------------------------------------------------------------- /rapporteur.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/rapporteur/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'rapporteur' 7 | spec.version = Rapporteur::VERSION 8 | spec.authors = ['Envy Labs'] 9 | spec.email = [''] 10 | spec.description = 'An engine that provides common status polling endpoint.' 11 | spec.summary = 'An engine that provides common status polling endpoint.' 12 | spec.homepage = 'https://github.com/envylabs/rapporteur' 13 | spec.license = 'MIT' 14 | spec.required_ruby_version = '>= 3.2' 15 | 16 | spec.metadata['source_code_uri'] = 'https://github.com/envylabs/rapporteur' 17 | spec.metadata['changelog_uri'] = 'https://github.com/envylabs/rapporteur/blob/main/CHANGELOG.md' 18 | 19 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 20 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 21 | end 22 | spec.bindir = 'exe' 23 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 24 | spec.require_paths = ['lib'] 25 | 26 | spec.add_dependency 'i18n', '>= 0.6', '< 2' 27 | 28 | spec.add_development_dependency 'appraisal', '~> 2.1' 29 | spec.add_development_dependency 'combustion', '~> 1.0' 30 | spec.add_development_dependency 'rake', '~> 13.0' 31 | spec.add_development_dependency 'rspec-collection_matchers', '~> 1.0' 32 | spec.add_development_dependency 'rspec-rails', '~> 7.0' 33 | spec.add_development_dependency 'rubocop', '~> 1.0' 34 | spec.add_development_dependency 'rubocop-rails_config', '~> 1.0' 35 | end 36 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "bump-minor-pre-major": true, 5 | "bump-patch-for-minor-pre-major": false, 6 | "changelog-path": "CHANGELOG.md", 7 | "component": "", 8 | "include-component-in-tag": false, 9 | "draft": false, 10 | "monorepo-tags": false, 11 | "package-name": "rapporteur", 12 | "prerelease": false, 13 | "release-type": "ruby", 14 | "version-file": "lib/rapporteur/version.rb" 15 | } 16 | }, 17 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 18 | } 19 | -------------------------------------------------------------------------------- /spec/internal/config/database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | test: 4 | adapter: "sqlite3" 5 | database: "db/combustion_test.sqlite" 6 | -------------------------------------------------------------------------------- /spec/internal/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | mount Rapporteur::Engine, at: '/status' 5 | end 6 | -------------------------------------------------------------------------------- /spec/internal/db/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.define do 4 | end 5 | -------------------------------------------------------------------------------- /spec/internal/log/.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /spec/internal/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/envylabs/rapporteur/927af131d583a87974f6b934e8c2d13265ab8a70/spec/internal/public/favicon.ico -------------------------------------------------------------------------------- /spec/models/check_list_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Rapporteur::CheckList, type: :model do 4 | let(:list) { described_class.new } 5 | 6 | context '#add' do 7 | it 'adds the given object to the list' do 8 | list.add(check = double) 9 | expect(list.to_a).to include(check) 10 | end 11 | 12 | it 'does not return duplicate entries' do 13 | list.add(check = double) 14 | list.add(check) 15 | expect(list.to_a).to have(1).check 16 | end 17 | end 18 | 19 | context '#clear' do 20 | it 'empties the list of objects' do 21 | list.add(double) 22 | expect { list.clear }.to change(list, :empty?).to(true) 23 | end 24 | end 25 | 26 | context '#each' do 27 | it 'yields each contained object in the order added' do 28 | list.add(check0 = double) 29 | list.add(check1 = double) 30 | list.add(check2 = double) 31 | returns = [] 32 | 33 | list.each do |object| 34 | returns << object 35 | end 36 | 37 | expect(returns[0]).to equal(check0) 38 | expect(returns[1]).to equal(check1) 39 | expect(returns[2]).to equal(check2) 40 | end 41 | 42 | it 'returns itself' do 43 | expect(list.each).to equal(list) 44 | end 45 | end 46 | 47 | context '#to_a' do 48 | it 'returns an Array' do 49 | expect(list.to_a).to be_an(Array) 50 | end 51 | 52 | it 'returns new different but equivalent Array instances' do 53 | return1 = list.to_a 54 | return2 = list.to_a 55 | expect(return1).to_not equal(return2) 56 | expect(return1).to eq(return2) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/models/message_list_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Rapporteur::MessageList, type: :model do 4 | let(:list) { Rapporteur::MessageList.new(:errors) } 5 | 6 | before do I18n.backend.reload! end 7 | 8 | context '#add' do 9 | it 'returns the MessageList instance.' do 10 | expect(list.add(:test, 'message')).to equal(list) 11 | end 12 | 13 | it 'adds a String message to the #full_messages' do 14 | list.add(:test, 'message') 15 | expect(list.full_messages).to include('test message') 16 | end 17 | 18 | it 'adds unrecognized Symbols as Strings to the #full_messages' do 19 | list.add(:test, :message) 20 | expect(list.full_messages).to include('test message') 21 | end 22 | 23 | it 'adds multiple messages to an attribute' do 24 | list.add(:test, 'message 1') 25 | list.add(:test, 'message 2') 26 | expect(list.full_messages).to include('test message 1') 27 | expect(list.full_messages).to include('test message 2') 28 | end 29 | 30 | it 'translates and adds recognized I18n keys in the #full_messages' do 31 | add_translation(:errors, :test, :i18n_message, 'translated') 32 | list.add(:test, :i18n_message) 33 | expect(list.full_messages).to include('test translated') 34 | end 35 | 36 | it 'translates and adds recognized I18n keys with named parameters in the #full_messages' do 37 | add_translation(:errors, :test, :i18n_message, 'translated %{language}') # rubocop:disable Style/FormatStringToken 38 | list.add(:test, :i18n_message, language: 'French') 39 | expect(list.full_messages).to include('test translated French') 40 | end 41 | 42 | it 'translates based on the initialized list type' do 43 | add_translation(:string, :test, :i18n_message, 'strings') 44 | add_translation(:messages, :test, :i18n_message, 'messages') 45 | add_translation(:errors, :test, :i18n_message, 'errors') 46 | message_list = Rapporteur::MessageList.new(:messages) 47 | error_list = Rapporteur::MessageList.new(:errors) 48 | message_list.add(:test, :i18n_message) 49 | error_list.add(:test, :i18n_message) 50 | expect(message_list.full_messages).to include('test messages') 51 | expect(message_list.full_messages).not_to include('test errors') 52 | expect(error_list.full_messages).to include('test errors') 53 | expect(error_list.full_messages).not_to include('test messages') 54 | end 55 | end 56 | 57 | context '#to_hash' do 58 | it 'flattens key/value pairs with only one value' do 59 | list.add(:test, 'message') 60 | expect(list.to_hash).to eq(test: 'message') 61 | end 62 | 63 | it 'retains multiple values for a single key' do 64 | list.add(:test, 'message1') 65 | list.add(:test, 'message2') 66 | result = list.to_hash 67 | expect(result).to have_key(:test) 68 | expect(result[:test]).to include('message1') 69 | expect(result[:test]).to include('message2') 70 | end 71 | 72 | it 'returns a new, unique, but equivalent Hash with each call' do 73 | list.add(:test, 'message') 74 | return1 = list.to_hash 75 | return2 = list.to_hash 76 | expect(return1).not_to equal(return2) 77 | expect(return1).to eq(return2) 78 | end 79 | end 80 | 81 | private 82 | 83 | def add_translation(type, key, message, string) 84 | I18n.backend.store_translations('en', rapporteur: { type.to_sym => { key.to_sym => { message.to_sym => string } } }) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/requests/active_record_check_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'A status request with an ActiveRecordCheck', type: :request do 4 | context 'with an unerring ActiveRecord connection' do 5 | before do 6 | Rapporteur.add_check(Rapporteur::Checks::ActiveRecordCheck) 7 | get(rapporteur.status_path(format: 'json')) 8 | end 9 | 10 | it_behaves_like 'a successful status response' 11 | end 12 | 13 | context 'with a failed ActiveRecord connection' do 14 | before do 15 | allow(ActiveRecord::Base.connection).to receive(:select_value) 16 | .and_raise(ActiveRecord::ConnectionNotEstablished) 17 | Rapporteur.add_check(Rapporteur::Checks::ActiveRecordCheck) 18 | get(rapporteur.status_path(format: 'json')) 19 | end 20 | 21 | it_behaves_like 'an erred status response' 22 | 23 | it 'contains a message regarding the database failure' do 24 | expect(response).to include_status_error_message(:database, I18n.t('rapporteur.errors.database.unavailable')) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/requests/halt_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'A check calling #halt!', type: :request do 4 | let(:parsed_response) { JSON.parse(response.body) } 5 | 6 | before do 7 | Rapporteur.add_check { |checker| checker.add_message(:one, 1) } 8 | Rapporteur.add_check { |checker| checker.add_message(:two, 2).halt! } 9 | Rapporteur.add_check { |checker| checker.add_message(:three, 3) } 10 | 11 | get(rapporteur.status_path(format: 'json')) 12 | end 13 | 14 | it 'runs the first check' do 15 | expect(parsed_response).to include('one') 16 | end 17 | 18 | it 'runs the second check' do 19 | expect(parsed_response).to include('two') 20 | end 21 | 22 | it 'does not run any checks after #halt! is called' do 23 | expect(parsed_response).not_to include('three') 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/requests/messsage_addition_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'A status request with a check that modifies messages', type: :request do 4 | context 'creating a message with a block' do 5 | context 'with an unerring response' do 6 | before do 7 | Rapporteur.add_check { |checker| checker.add_message('git_repo', 'git@github.com:organization/repo.git') } 8 | get(rapporteur.status_path(format: 'json')) 9 | end 10 | 11 | it_behaves_like 'a successful status response' 12 | 13 | it "responds with the check's messages" do 14 | expect(response).to include_status_message('git_repo', 'git@github.com:organization/repo.git') 15 | end 16 | end 17 | 18 | context 'with an erring response' do 19 | before do 20 | Rapporteur.add_check { |checker| checker.add_error(:base, 'failed') } 21 | get(rapporteur.status_path(format: 'json')) 22 | end 23 | 24 | it_behaves_like 'an erred status response' 25 | 26 | it "does not respond with the check's messages" do 27 | expect(response).not_to include_status_message('git_repo', 'git@github.com:organization/repo.git') 28 | end 29 | end 30 | 31 | context 'with no message-modifying checks' do 32 | before do 33 | get(rapporteur.status_path(format: 'json')) 34 | end 35 | 36 | it_behaves_like 'a successful status response' 37 | 38 | it 'does not respond with a messages list' do 39 | expect(JSON.parse(response.body)).not_to(have_key('messages')) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/requests/no_checks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'A status request with no checks', type: :request do 4 | before do 5 | get(rapporteur.status_path(format: 'json')) 6 | end 7 | 8 | it_behaves_like 'a successful status response' 9 | end 10 | -------------------------------------------------------------------------------- /spec/requests/revision_check_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'A status request with a RevisionCheck', type: :request do 4 | before do 5 | allow(Rapporteur::Revision).to receive(:current).and_return('revisionidentifier') 6 | Rapporteur.add_check(Rapporteur::Checks::RevisionCheck) 7 | 8 | get(rapporteur.status_path(format: 'json')) 9 | end 10 | 11 | it_behaves_like 'a successful status response' 12 | 13 | context 'the response payload' do 14 | it 'contains the current application revision' do 15 | expect(response).to include_status_message('revision', 'revisionidentifier') 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/requests/sinatra_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RACK_ENV'] = 'test' 4 | 5 | begin 6 | require 'sinatra/base' 7 | require 'rack/test' 8 | 9 | RSpec.describe 'Sinatra' do 10 | include Rack::Test::Methods 11 | 12 | class TestApp < Sinatra::Base 13 | set :host_authorization, { permitted_hosts: [] } 14 | 15 | get '/status.json' do 16 | content_type :json 17 | body Rapporteur.run.as_json.to_json 18 | end 19 | end 20 | 21 | def app 22 | Rack::Lint.new(TestApp) 23 | end 24 | 25 | before do 26 | Rapporteur.add_check(Rapporteur::Checks::TimeCheck) 27 | end 28 | 29 | it 'responds with an HTTP 200 JSON response' do 30 | make_request 31 | 32 | expect(last_response).to have_attributes( 33 | content_type: Mime[:json], 34 | status: 200 35 | ) 36 | end 37 | 38 | it 'responds with valid JSON' do 39 | make_request 40 | expect { JSON.parse(last_response.body) }.not_to(raise_error) 41 | end 42 | 43 | it 'contains the time in ISO8601' do 44 | allow(Time).to receive(:now).and_return(Time.gm(2013, 8, 23)) 45 | make_request 46 | 47 | expect(last_response).to include_status_message('time', /^2013-08-23T00:00:00(?:.000)?Z$/) 48 | end 49 | 50 | context 'the response payload' do 51 | it 'does not contain errors' do 52 | make_request 53 | expect(JSON.parse(last_response.body)).not_to(have_key('errors')) 54 | end 55 | end 56 | 57 | private 58 | 59 | def make_request 60 | get('/status.json') 61 | end 62 | end 63 | rescue LoadError 64 | end 65 | -------------------------------------------------------------------------------- /spec/requests/time_check_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'A status request with a TimeCheck', type: :request do 4 | before do 5 | Rapporteur.add_check(Rapporteur::Checks::TimeCheck) 6 | 7 | allow(Time).to receive(:now).and_return(Time.gm(2013, 8, 23)) 8 | 9 | get(rapporteur.status_path(format: 'json')) 10 | end 11 | 12 | it_behaves_like 'a successful status response' 13 | 14 | context 'the response payload' do 15 | it 'contains the time in ISO8601' do 16 | expect(response).to include_status_message('time', /^2013-08-23T00:00:00(?:.000)?Z$/) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/route_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RouteHelper 4 | def self.included(base) 5 | base.routes { Rapporteur::Engine.routes } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/routing/routes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe 'status route', type: :routing do 4 | it 'routes / to rapporteur/statuses#show' do 5 | expect(get: '/').to route_to(action: 'show', 6 | controller: 'rapporteur/statuses') 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'combustion' 5 | 6 | ENV['RAILS_ENV'] ||= 'test' 7 | Combustion.initialize! :action_controller, :active_record 8 | 9 | require 'rspec/rails' 10 | require 'rspec/collection_matchers' 11 | require 'rapporteur/rspec3' 12 | require 'route_helper' 13 | 14 | RSpec.configure do |config| 15 | config.disable_monkey_patching! 16 | config.example_status_persistence_file_path = '.rspec_status' 17 | config.run_all_when_everything_filtered = true 18 | config.filter_run :focus 19 | 20 | config.order = 'random' 21 | 22 | config.expect_with(:rspec) do |c| 23 | c.syntax = :expect 24 | end 25 | 26 | config.before { Rapporteur.clear_checks } 27 | 28 | config.include RouteHelper, type: :controller 29 | config.include RouteHelper, type: :routing 30 | end 31 | --------------------------------------------------------------------------------