├── .circleci ├── config.yml ├── gemspecs │ ├── compatible │ └── latest ├── linter_configs │ ├── .bundler-audit.yml │ ├── .commitspell.yml │ ├── .cspell.yml │ ├── .fasterer.yml │ ├── .lefthook.yml │ ├── .markdownlint.yml │ ├── .rubocop.yml │ └── .yamllint.yml └── scripts │ ├── changeloglint.sh │ ├── commitspell.sh │ ├── release.sh │ └── set_publisher_credentials.sh ├── .codeclimate.yml ├── .github ├── BRANCH_NAMING_CONVENTION.md ├── DEVELOPMENT_ENVIRONMENT_GUIDE.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ ├── issue_report.md │ └── question.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .reek.yml ├── .rspec ├── .ruby-gemset ├── .ruby-version ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── healthcheck_endpoint.gemspec ├── lib ├── healthcheck_endpoint.rb └── healthcheck_endpoint │ ├── configuration.rb │ ├── core.rb │ ├── error │ └── configuration │ │ ├── argument_type.rb │ │ ├── enpoint_pattern.rb │ │ ├── http_status_failure.rb │ │ ├── http_status_success.rb │ │ ├── not_callable_service.rb │ │ ├── not_configured.rb │ │ └── unknown_service.rb │ ├── rack_middleware.rb │ ├── resolver.rb │ └── version.rb └── spec ├── healthcheck_endpoint ├── configuration_spec.rb ├── error │ └── configuration │ │ ├── argument_type_spec.rb │ │ ├── http_status_failure_spec.rb │ │ ├── http_status_success_spec.rb │ │ ├── not_callable_service_spec.rb │ │ ├── not_configured_spec.rb │ │ └── unknown_service_spec.rb ├── rack_middleware_spec.rb ├── resolver_spec.rb ├── rspec_helper │ ├── configuration_spec.rb │ └── context_generator_spec.rb └── version_spec.rb ├── healthcheck_endpoint_spec.rb ├── spec_helper.rb └── support ├── config ├── bundler.rb ├── ffaker.rb ├── json_matchers.rb ├── pry.rb └── simplecov.rb ├── helpers ├── configuration.rb └── context_generator.rb └── schemas └── jsonapi_response.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | version: 2.1 4 | 5 | defaults: &defaults 6 | working_directory: ~/healthcheck-endpoint 7 | docker: 8 | - image: cimg/ruby:<< parameters.ruby-version >> 9 | 10 | orbs: 11 | ruby: circleci/ruby@2.3.0 12 | 13 | references: 14 | bundle_install: &bundle_install 15 | run: 16 | name: Installing gems 17 | command: | 18 | bundle config set --local path '~/vendor/bundle' 19 | bundle install 20 | 21 | install_linters: &install_linters 22 | run: 23 | name: Installing bunch of linters 24 | command: | 25 | curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.deb.sh' | sudo -E bash 26 | sudo apt-get update -y 27 | sudo apt-get install -y lefthook shellcheck yamllint 28 | npm install --prefix='~/.local' --global --save-dev git+https://github.com/streetsidesoftware/cspell-cli markdownlint-cli 29 | cp .circleci/linter_configs/.fasterer.yml .fasterer.yml 30 | cp .circleci/linter_configs/.lefthook.yml lefthook.yml 31 | 32 | install_codeclimate_reporter: &install_codeclimate_reporter 33 | run: 34 | name: Installing CodeClimate test reporter 35 | command: | 36 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 37 | chmod +x ./cc-test-reporter 38 | 39 | use_latest_bundler: &use_latest_bundler 40 | run: 41 | name: Using latest bundler 42 | command: gem install bundler 43 | 44 | use_latest_gemspec: &use_latest_gemspec 45 | run: 46 | name: Using latest gemspec 47 | command: cp .circleci/gemspecs/latest healthcheck_endpoint.gemspec 48 | 49 | use_compatible_gemspec: &use_compatible_gemspec 50 | run: 51 | name: Using compatible gemspec 52 | command: cp .circleci/gemspecs/compatible healthcheck_endpoint.gemspec 53 | 54 | jobs: 55 | linters-ruby: 56 | parameters: 57 | ruby-version: 58 | type: string 59 | 60 | <<: *defaults 61 | 62 | steps: 63 | - checkout 64 | 65 | - <<: *use_latest_bundler 66 | - <<: *use_latest_gemspec 67 | - <<: *bundle_install 68 | - <<: *install_linters 69 | 70 | - run: 71 | name: Running commit linters 72 | command: lefthook run commit-linters 73 | 74 | - run: 75 | name: Running code style linters 76 | command: lefthook run code-style-linters 77 | 78 | - run: 79 | name: Running code performance linters 80 | command: lefthook run code-performance-linters 81 | 82 | - run: 83 | name: Running code vulnerability linters 84 | command: lefthook run code-vulnerability-linters 85 | 86 | - run: 87 | name: Running code documentation linters 88 | command: lefthook run code-documentation-linters 89 | 90 | - run: 91 | name: Running release linters 92 | command: lefthook run release-linters 93 | 94 | tests-ruby: 95 | parameters: 96 | ruby-version: 97 | type: string 98 | 99 | <<: *defaults 100 | 101 | steps: 102 | - checkout 103 | 104 | - <<: *use_latest_bundler 105 | - <<: *use_latest_gemspec 106 | - <<: *bundle_install 107 | - <<: *install_codeclimate_reporter 108 | 109 | - run: 110 | name: Running RSpec 111 | command: | 112 | ./cc-test-reporter before-build 113 | bundle exec rspec 114 | 115 | - run: 116 | name: Creating CodeClimate test coverage report 117 | command: | 118 | ./cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json" 119 | 120 | - store_artifacts: 121 | name: Saving Simplecov coverage artifacts 122 | path: ~/healthcheck-endpoint/coverage 123 | destination: coverage 124 | 125 | - deploy: 126 | name: Uploading CodeClimate test coverage report 127 | command: | 128 | ./cc-test-reporter sum-coverage --output - --parts $CIRCLE_NODE_TOTAL coverage/codeclimate.*.json | ./cc-test-reporter upload-coverage --debug --input - 129 | 130 | compatibility-ruby: 131 | parameters: 132 | ruby-version: 133 | type: string 134 | 135 | <<: *defaults 136 | 137 | steps: 138 | - checkout 139 | 140 | - <<: *use_compatible_gemspec 141 | 142 | - ruby/install-deps: 143 | bundler-version: "2.3.26" 144 | with-cache: false 145 | path: '~/vendor/custom_bundle' 146 | 147 | - run: 148 | name: Running compatibility tests 149 | command: bundle exec rspec 150 | 151 | rubygems-deps-ruby: 152 | parameters: 153 | ruby-version: 154 | type: string 155 | 156 | <<: *defaults 157 | 158 | steps: 159 | - checkout 160 | 161 | - run: 162 | name: Building rubygems dependencies from default gemspec on minimal Ruby version 163 | command: bundle install 164 | 165 | releasing-gem-from-ruby: 166 | parameters: 167 | ruby-version: 168 | type: string 169 | 170 | <<: *defaults 171 | 172 | steps: 173 | - checkout 174 | 175 | - add_ssh_keys: 176 | fingerprints: 177 | - "SHA256:eJhlVtu2gws5rDavHcqZ5GJF/aS8kCctMprdC+Twlns" 178 | 179 | - run: 180 | name: Publishing new release 181 | command: ./.circleci/scripts/release.sh 182 | 183 | workflows: 184 | build_test_deploy: 185 | jobs: 186 | - linters-ruby: 187 | matrix: 188 | parameters: 189 | ruby-version: ["3.3-node"] 190 | - tests-ruby: 191 | matrix: 192 | parameters: 193 | ruby-version: ["3.3"] 194 | - compatibility-ruby: 195 | matrix: 196 | parameters: 197 | ruby-version: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2"] 198 | - rubygems-deps-ruby: 199 | matrix: 200 | parameters: 201 | ruby-version: ["2.5"] 202 | - releasing-gem-from-ruby: 203 | requires: 204 | - linters-ruby 205 | - tests-ruby 206 | - compatibility-ruby 207 | - rubygems-deps-ruby 208 | matrix: 209 | parameters: 210 | ruby-version: ["2.5"] 211 | filters: 212 | branches: 213 | only: master 214 | -------------------------------------------------------------------------------- /.circleci/gemspecs/compatible: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/healthcheck_endpoint/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'healthcheck_endpoint' 7 | spec.version = HealthcheckEndpoint::VERSION 8 | spec.authors = ['Vladislav Trotsenko'] 9 | spec.email = %w[admin@bestweb.com.ua] 10 | spec.summary = %(Simple configurable application healthcheck rack middleware) 11 | spec.description = %(Simple configurable application healthcheck rack middleware.) 12 | spec.homepage = 'https://github.com/obstools/healthcheck-endpoint' 13 | spec.license = 'MIT' 14 | 15 | spec.required_ruby_version = '>= 2.5.0' 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.require_paths = %w[lib] 18 | 19 | spec.add_runtime_dependency 'rack', '>= 2.0.1' 20 | 21 | spec.add_development_dependency 'ffaker' 22 | spec.add_development_dependency 'json_matchers' 23 | spec.add_development_dependency 'rake' 24 | spec.add_development_dependency 'rspec' 25 | end 26 | -------------------------------------------------------------------------------- /.circleci/gemspecs/latest: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/healthcheck_endpoint/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'healthcheck_endpoint' 7 | spec.version = HealthcheckEndpoint::VERSION 8 | spec.authors = ['Vladislav Trotsenko'] 9 | spec.email = %w[admin@bestweb.com.ua] 10 | spec.summary = %(Simple configurable application healthcheck rack middleware) 11 | spec.description = %(Simple configurable application healthcheck rack middleware.) 12 | spec.homepage = 'https://github.com/obstools/healthcheck-endpoint' 13 | spec.license = 'MIT' 14 | 15 | spec.required_ruby_version = '>= 2.5.0' 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.require_paths = %w[lib] 18 | 19 | spec.add_runtime_dependency 'rack', '>= 2.0.1' 20 | 21 | spec.add_development_dependency 'bundler-audit', '~> 0.9.2' 22 | spec.add_development_dependency 'fasterer', '~> 0.11.0' 23 | spec.add_development_dependency 'ffaker', '~> 2.23' 24 | spec.add_development_dependency 'json_matchers', '~> 0.11.1' 25 | spec.add_development_dependency 'pry-byebug', '~> 3.10', '>= 3.10.1' 26 | spec.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1' 27 | spec.add_development_dependency 'reek', '~> 6.3' 28 | spec.add_development_dependency 'rspec', '~> 3.13' 29 | spec.add_development_dependency 'rubocop', '~> 1.67' 30 | spec.add_development_dependency 'rubocop-performance', '~> 1.22', '>= 1.22.1' 31 | spec.add_development_dependency 'rubocop-rspec', '~> 3.1' 32 | spec.add_development_dependency 'simplecov', '~> 0.22.0' 33 | end 34 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.bundler-audit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ignore: 4 | - EXA-MPLE-XXXX 5 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.commitspell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | enableGlobDot: true 4 | 5 | patterns: 6 | - name: GithubUser 7 | pattern: /\[@.+\]/gmx 8 | 9 | languageSettings: 10 | - languageId: markdown 11 | ignoreRegExpList: 12 | - Email 13 | - GithubUser 14 | 15 | words: 16 | - Vladislav 17 | - Trotsenko 18 | - bagage 19 | - bagages 20 | - bestwebua 21 | - codebases 22 | - codeclimate 23 | - commitspell 24 | - creds 25 | - gemspecs 26 | - healthcheck 27 | - lefthook 28 | - markdownlint 29 | - rubocop 30 | - simplecov 31 | - stdlib 32 | - obstools 33 | - yamlint 34 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.cspell.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | enableGlobDot: true 4 | 5 | patterns: 6 | - name: GithubUser 7 | pattern: /\[@.+\]/gmx 8 | - name: MarkdownCode 9 | pattern: /`{1,3}.+`{1,3}/gmx 10 | - name: MarkdownCodeBlock 11 | pattern: /^\s*```[\s\S]*?^\s*```/gmx 12 | 13 | languageSettings: 14 | - languageId: markdown 15 | ignoreRegExpList: 16 | - Email 17 | - GithubUser 18 | - MarkdownCode 19 | - MarkdownCodeBlock 20 | 21 | words: 22 | - Vladislav 23 | - Trotsenko 24 | - bestwebua 25 | - codebases 26 | - commiting 27 | - gemspecs 28 | - hanami 29 | - healthcheck 30 | - healthchecks 31 | - obstools 32 | - roda 33 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.fasterer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | exclude_paths: 4 | - '.circleci/**/*.rb' 5 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.lefthook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | no_tty: true 4 | skip_output: 5 | - meta 6 | 7 | commit-linters: 8 | commands: 9 | commitspell: 10 | run: .circleci/scripts/commitspell.sh -c '.circleci/linter_configs/.commitspell.yml' 11 | 12 | code-style-linters: 13 | commands: 14 | reek: 15 | run: bundle exec reek 16 | rubocop: 17 | run: bundle exec rubocop -c '.circleci/linter_configs/.rubocop.yml' 18 | shellcheck: 19 | glob: '*.{sh}' 20 | run: shellcheck --norc {all_files} 21 | yamllint: 22 | run: yamllint -c '.circleci/linter_configs/.yamllint.yml' . 23 | 24 | code-performance-linters: 25 | commands: 26 | fasterer: 27 | run: bundle exec fasterer 28 | 29 | code-vulnerability-linters: 30 | commands: 31 | bundle-audit: 32 | run: bundle exec bundle-audit check -c '.circleci/linter_configs/.bundler-audit.yml' --update 33 | 34 | code-documentation-linters: 35 | commands: 36 | cspell: 37 | run: cspell-cli lint -c '.circleci/linter_configs/.cspell.yml' '**/*.{txt,md}' 38 | markdownlint: 39 | run: markdownlint -c '.circleci/linter_configs/.markdownlint.yml' '**/*.md' 40 | 41 | release-linters: 42 | commands: 43 | changeloglint: 44 | run: .circleci/scripts/changeloglint.sh 45 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | default: true 4 | 5 | MD013: 6 | line_length: 500 7 | 8 | MD024: 9 | siblings_only: true 10 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | require: 4 | - rubocop-rspec 5 | - rubocop-performance 6 | 7 | AllCops: 8 | DisplayCopNames: true 9 | DisplayStyleGuide: true 10 | TargetRubyVersion: 2.5 11 | SuggestExtensions: false 12 | NewCops: enable 13 | 14 | # Metrics --------------------------------------------------------------------- 15 | 16 | Metrics/ClassLength: 17 | Max: 150 18 | 19 | Metrics/MethodLength: 20 | Max: 15 21 | 22 | Metrics/BlockLength: 23 | Enabled: false 24 | 25 | Metrics/CyclomaticComplexity: 26 | Enabled: false 27 | 28 | Metrics/PerceivedComplexity: 29 | Enabled: false 30 | 31 | # Naming ---------------------------------------------------------------------- 32 | 33 | Naming/VariableNumber: 34 | Enabled: false 35 | 36 | Naming/RescuedExceptionsVariableName: 37 | Enabled: false 38 | 39 | Naming/InclusiveLanguage: 40 | Enabled: false 41 | 42 | # Style ----------------------------------------------------------------------- 43 | 44 | Style/Documentation: 45 | Enabled: false 46 | 47 | Style/DoubleNegation: 48 | Enabled: false 49 | 50 | Style/EmptyCaseCondition: 51 | Enabled: false 52 | 53 | Style/ParallelAssignment: 54 | Enabled: false 55 | 56 | Style/RescueStandardError: 57 | Enabled: false 58 | 59 | Style/RedundantConstantBase: 60 | Enabled: false 61 | 62 | # Layout ---------------------------------------------------------------------- 63 | 64 | Layout/LineLength: 65 | Max: 150 66 | 67 | Layout/ClassStructure: 68 | Enabled: true 69 | Categories: 70 | module_inclusion: 71 | - include 72 | - prepend 73 | - extend 74 | ExpectedOrder: 75 | - module_inclusion 76 | - constants 77 | - public_class_methods 78 | - initializer 79 | - public_methods 80 | - protected_methods 81 | - private_methods 82 | 83 | Layout/EmptyLineAfterGuardClause: 84 | Enabled: false 85 | 86 | # Lint ------------------------------------------------------------------------ 87 | 88 | Lint/NoReturnInBeginEndBlocks: 89 | Enabled: false 90 | 91 | # Gemspec --------------------------------------------------------------------- 92 | 93 | Gemspec/RequireMFA: 94 | Enabled: false 95 | 96 | Gemspec/DevelopmentDependencies: 97 | Enabled: false 98 | 99 | Gemspec/AddRuntimeDependency: 100 | Enabled: false 101 | 102 | # Performance ----------------------------------------------------------------- 103 | 104 | Performance/MethodObjectAsBlock: 105 | Enabled: false 106 | 107 | # RSpec ----------------------------------------------------------------------- 108 | 109 | RSpec/ExampleLength: 110 | Enabled: false 111 | 112 | RSpec/NestedGroups: 113 | Enabled: false 114 | 115 | RSpec/MultipleExpectations: 116 | Enabled: false 117 | 118 | RSpec/MessageChain: 119 | Enabled: false 120 | 121 | RSpec/ContextWording: 122 | Enabled: false 123 | 124 | RSpec/AnyInstance: 125 | Enabled: false 126 | 127 | RSpec/MessageSpies: 128 | Enabled: false 129 | 130 | RSpec/MultipleDescribes: 131 | Enabled: false 132 | 133 | RSpec/MultipleMemoizedHelpers: 134 | Enabled: false 135 | 136 | RSpec/StubbedMock: 137 | Enabled: false 138 | 139 | RSpec/VerifiedDoubleReference: 140 | Enabled: false 141 | 142 | RSpec/StringAsInstanceDoubleConstant: 143 | Enabled: false 144 | -------------------------------------------------------------------------------- /.circleci/linter_configs/.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | extends: default 4 | 5 | rules: 6 | line-length: 7 | max: 200 8 | -------------------------------------------------------------------------------- /.circleci/scripts/changeloglint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | changelog=$(if [ "$1" = "" ]; then echo "CHANGELOG.md"; else echo "$1"; fi) 5 | 6 | get_current_gem_version() { 7 | ruby -r rubygems -e "puts Gem::Specification::load('$(ls -- *.gemspec)').version" 8 | } 9 | 10 | latest_changelog_tag() { 11 | grep -Po "(?<=\#\# \[)[0-9]+\.[0-9]+\.[0-9]+?(?=\])" "$changelog" | head -n 1 12 | } 13 | 14 | current_gem_version="$(get_current_gem_version)" 15 | 16 | if [ "$current_gem_version" = "$(latest_changelog_tag)" ] 17 | then 18 | echo "SUCCESS: Current gem version ($current_gem_version) has been found on the top of project changelog." 19 | else 20 | echo "FAILURE: Following to \"Keep a Changelog\" convention current gem version ($current_gem_version) must be mentioned on the top of project changelog." 21 | exit 1 22 | fi 23 | -------------------------------------------------------------------------------- /.circleci/scripts/commitspell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | configuration=$(if [ "$2" = "" ]; then echo "$2"; else echo " $1 $2"; fi) 5 | latest_commit=$(git rev-parse HEAD) 6 | 7 | spellcheck_info() { 8 | echo "Checking the spelling of the latest commit ($latest_commit) message..." 9 | } 10 | 11 | compose_cspell_command() { 12 | echo "cspell-cli lint stdin$configuration" 13 | } 14 | 15 | cspell="$(compose_cspell_command)" 16 | 17 | spellcheck_latest_commit() { 18 | git log -1 --pretty=%B | $cspell 19 | } 20 | 21 | spellcheck_info 22 | spellcheck_latest_commit 23 | -------------------------------------------------------------------------------- /.circleci/scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | GH_CLI_RELEASES_URL="https://github.com/cli/cli/releases" 5 | FILE_NAME="gh" 6 | BUILD_ARCHITECTURE="linux_amd64.deb" 7 | DELIMETER="_" 8 | PACKAGE_FILE="$FILE_NAME$DELIMETER$BUILD_ARCHITECTURE" 9 | 10 | gh_cli_latest_release() { 11 | curl -sL -o /dev/null -w '%{url_effective}' "$GH_CLI_RELEASES_URL/latest" | rev | cut -f 1 -d '/'| rev 12 | } 13 | 14 | download_gh_cli() { 15 | test -z "$VERSION" && VERSION="$(gh_cli_latest_release)" 16 | test -z "$VERSION" && { 17 | echo "Unable to get GitHub CLI release." >&2 18 | exit 1 19 | } 20 | curl -s -L -o "$PACKAGE_FILE" "$GH_CLI_RELEASES_URL/download/$VERSION/$FILE_NAME$DELIMETER$(printf '%s' "$VERSION" | cut -c 2-100)$DELIMETER$BUILD_ARCHITECTURE" 21 | } 22 | 23 | install_gh_cli() { 24 | sudo dpkg -i "$PACKAGE_FILE" 25 | rm "$PACKAGE_FILE" 26 | } 27 | 28 | get_release_candidate_version() { 29 | ruby -r rubygems -e "puts Gem::Specification::load('$(ls -- *.gemspec)').version" 30 | } 31 | 32 | release_candidate_tag="v$(get_release_candidate_version)" 33 | 34 | is_an_existing_github_release() { 35 | git fetch origin "refs/tags/$release_candidate_tag" >/dev/null 2>&1 36 | } 37 | 38 | release_to_rubygems() { 39 | echo "Setting RubyGems publisher credentials..." 40 | ./.circleci/scripts/set_publisher_credentials.sh 41 | echo "Preparation for release..." 42 | git config --global user.email "${PUBLISHER_EMAIL}" 43 | git config --global user.name "${PUBLISHER_NAME}" 44 | git stash 45 | gem install yard gem-ctags 46 | bundle install 47 | echo "Publishing new gem release to RubyGems..." 48 | rake release 49 | } 50 | 51 | release_to_github() { 52 | echo "Downloading and installing latest gh cli..." 53 | download_gh_cli 54 | install_gh_cli 55 | echo "Publishing new release notes to GitHub..." 56 | gh release create "$release_candidate_tag" --generate-notes 57 | } 58 | 59 | update_develop_branch() { 60 | echo "Updating develop branch with new release tag..." 61 | git checkout develop 62 | git merge "$release_candidate_tag" --ff --no-edit 63 | git push origin develop 64 | } 65 | 66 | if is_an_existing_github_release 67 | then echo "Tag $release_candidate_tag already exists on GitHub. Skipping releasing flow..." 68 | else release_to_rubygems; release_to_github; update_develop_branch 69 | fi 70 | -------------------------------------------------------------------------------- /.circleci/scripts/set_publisher_credentials.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set +x 4 | mkdir -p ~/.gem 5 | 6 | cat << EOF > ~/.gem/credentials 7 | --- 8 | :rubygems_api_key: ${RUBYGEMS_API_KEY} 9 | EOF 10 | 11 | chmod 0600 ~/.gem/credentials 12 | set -x 13 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | checks: 4 | argument-count: 5 | enabled: false 6 | method-complexity: 7 | enabled: false 8 | 9 | plugins: 10 | rubocop: 11 | enabled: true 12 | channel: rubocop-1-67 13 | config: 14 | file: .circleci/linter_configs/.rubocop.yml 15 | 16 | reek: 17 | enabled: true 18 | -------------------------------------------------------------------------------- /.github/BRANCH_NAMING_CONVENTION.md: -------------------------------------------------------------------------------- 1 | # Branch naming convention 2 | 3 | ## Branch naming 4 | 5 | > Please note for new pull requests create new branches from current `develop` branch only. 6 | 7 | Branch name should include type of your contribution and context. Please follow next pattern for naming your branches: 8 | 9 | ```bash 10 | feature/add-some-feature 11 | technical/some-technical-improvements 12 | bugfix/fix-some-bug-name 13 | ``` 14 | 15 | ## Before PR actions 16 | 17 | ### Squash commits 18 | 19 | Please squash all branch commits into the one before opening your PR from your fork. It's simple to do with the git: 20 | 21 | ```bash 22 | git rebase -i [hash your first commit of your branch]~1 23 | git rebase -i 6467fe36232401fa740af067cfd8ac9ec932fed2~1 # example 24 | ``` 25 | 26 | ### Add commit description 27 | 28 | Please complete your commit description following next pattern: 29 | 30 | ```code 31 | Technical/Add info files # should be the same name as your branch name 32 | 33 | * Added license, changelog, contributing, code of conduct docs 34 | * Added GitHub templates 35 | * Updated project license link 36 | ``` 37 | -------------------------------------------------------------------------------- /.github/DEVELOPMENT_ENVIRONMENT_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Development environment guide 2 | 3 | ## Preparing 4 | 5 | Clone `healthcheck-endpoint` repository: 6 | 7 | ```bash 8 | git clone https://github.com/obstools/healthcheck-endpoint.git 9 | cd ruby-gem 10 | ``` 11 | 12 | Configure latest Ruby environment: 13 | 14 | ```bash 15 | echo 'ruby-3.2.0' > .ruby-version 16 | cp .circleci/gemspec_latest healthcheck_endpoint.gemspec 17 | ``` 18 | 19 | ## Commiting 20 | 21 | Commit your changes excluding `.ruby-version`, `healthcheck_endpoint.gemspec` 22 | 23 | ```bash 24 | git add . ':!.ruby-version' ':!healthcheck_endpoint.gemspec' 25 | git commit -m 'Your new awesome healthcheck_endpoint feature' 26 | ``` 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | github: [bestwebua] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] Your bug report title here" 5 | labels: bug 6 | assignees: bestwebua 7 | 8 | --- 9 | 10 | 11 | 12 | ### New bug checklist 13 | 14 | - [ ] I have updated `healthcheck_endpoint` to the latest version 15 | - [ ] I have read the [Contribution Guidelines](https://github.com/obstools/healthcheck-endpoint/blob/master/CONTRIBUTING.md) 16 | - [ ] I have read the [documentation](https://github.com/obstools/healthcheck-endpoint/blob/master/README.md) 17 | - [ ] I have searched for [existing GitHub issues](https://github.com/obstools/healthcheck-endpoint/issues) 18 | 19 | 20 | 21 | ### Bug description 22 | 23 | 24 | ##### Complete output when running `healthcheck_endpoint`, including the stack trace and command used 25 | 26 |
27 |
[INSERT OUTPUT HERE]
28 |
29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea 4 | title: "[FEATURE] Your feature request title here" 5 | labels: enhancement 6 | assignees: bestwebua 7 | 8 | --- 9 | 10 | 11 | 12 | ### New feature request checklist 13 | 14 | - [ ] I have updated `healthcheck_endpoint` to the latest version 15 | - [ ] I have read the [Contribution Guidelines](https://github.com/obstools/healthcheck-endpoint/blob/master/CONTRIBUTING.md) 16 | - [ ] I have read the [documentation](https://github.com/obstools/healthcheck-endpoint/blob/master/README.md) 17 | - [ ] I have searched for [existing GitHub issues](https://github.com/obstools/healthcheck-endpoint/issues) 18 | 19 | 20 | 21 | ### Feature description 22 | 23 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Create a report to help us improve 4 | title: "[ISSUE] Your issue report title here" 5 | labels: '' 6 | assignees: bestwebua 7 | 8 | --- 9 | 10 | 11 | 12 | ### New issue checklist 13 | 14 | - [ ] I have updated `healthcheck_endpoint` to the latest version 15 | - [ ] I have read the [Contribution Guidelines](https://github.com/obstools/healthcheck-endpoint/blob/master/CONTRIBUTING.md) 16 | - [ ] I have read the [documentation](https://github.com/obstools/healthcheck-endpoint/blob/master/README.md) 17 | - [ ] I have searched for [existing GitHub issues](https://github.com/obstools/healthcheck-endpoint/issues) 18 | 19 | 20 | 21 | ### Issue description 22 | 23 | 24 | ##### Complete output when running `healthcheck_endpoint`, including the stack trace and command used 25 | 26 |
27 | 28 | ```code 29 | [INSERT OUTPUT HERE] 30 | ``` 31 | 32 |
33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask your question to team 4 | title: "[QUESTION] Your question title here" 5 | labels: question 6 | assignees: bestwebua 7 | 8 | --- 9 | 10 | 11 | 12 | ### New question checklist 13 | 14 | - [ ] I have read the [Contribution Guidelines](https://github.com/obstools/healthcheck-endpoint/blob/master/CONTRIBUTING.md) 15 | - [ ] I have read the [documentation](https://github.com/obstools/healthcheck-endpoint/blob/master/README.md) 16 | - [ ] I have searched for [existing GitHub issues](https://github.com/obstools/healthcheck-endpoint/issues) 17 | 18 | 19 | 20 | ### Question 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # PR Details 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Description 9 | 10 | 11 | 12 | ## Related Issue 13 | 14 | 15 | 16 | 17 | 18 | 19 | ## Motivation and Context 20 | 21 | 22 | 23 | ## How Has This Been Tested 24 | 25 | 26 | 27 | 28 | 29 | ## Types of changes 30 | 31 | 32 | 33 | - [ ] Docs change / refactoring / dependency upgrade 34 | - [ ] Bug fix (non-breaking change which fixes an issue) 35 | - [ ] New feature (non-breaking change which adds functionality) 36 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 37 | 38 | ## Checklist 39 | 40 | 41 | 42 | 43 | - [ ] My code follows the code style of this project 44 | - [ ] My change requires a change to the documentation 45 | - [ ] I have updated the documentation accordingly 46 | - [ ] I have read the [**CONTRIBUTING** document](https://github.com/obstools/healthcheck-endpoint/blob/master/CONTRIBUTING.md) 47 | - [ ] I have added tests to cover my changes 48 | - [ ] I have run `bundle exec rspec` from the root directory to see all new and existing tests pass 49 | - [ ] I have run `rubocop` and `reek` to ensure the code style is valid 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | .rspec_status 10 | .DS_Store 11 | Gemfile.lock 12 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | detectors: 4 | IrresponsibleModule: 5 | enabled: false 6 | 7 | ControlParameter: 8 | exclude: 9 | - HealthcheckEndpoint::Configuration#raise_unless 10 | - HealthcheckEndpoint::Configuration#validator_argument_type 11 | 12 | LongParameterList: 13 | exclude: 14 | - HealthcheckEndpoint::Configuration#raise_unless 15 | 16 | ManualDispatch: 17 | exclude: 18 | - HealthcheckEndpoint::Configuration#validator_services_callable 19 | 20 | TooManyConstants: 21 | exclude: 22 | - HealthcheckEndpoint::Configuration 23 | 24 | TooManyStatements: 25 | exclude: 26 | - HealthcheckEndpoint::Configuration#validate_attribute 27 | 28 | UtilityFunction: 29 | exclude: 30 | - HealthcheckEndpoint::Configuration#build_configuration_settings 31 | - HealthcheckEndpoint::Configuration#validator_argument_type 32 | - HealthcheckEndpoint::Configuration#validator_endpoint 33 | - HealthcheckEndpoint::Configuration#validator_http_status_failure 34 | - HealthcheckEndpoint::Configuration#validator_http_status_success 35 | - HealthcheckEndpoint::Configuration#validator_services_callable 36 | - HealthcheckEndpoint::Resolver#configuration 37 | 38 | exclude_paths: 39 | - spec/support/helpers 40 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | healthcheck_endpoint 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.5.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## [1.0.0] - 2024-10-25 6 | 7 | ### Updated 8 | 9 | - Updated gem name to `healthcheck_endpoint`, namespace to `HealthcheckEndpoint` 10 | - Updated gem runtime/development dependencies 11 | - Updated gem documentation 12 | 13 | ## [0.3.0] - 2024-04-15 14 | 15 | ### Added 16 | 17 | - Added ability to show in the response current probe type 18 | 19 | ```json 20 | { 21 | "data": { 22 | "id": "a09efd18-e09f-4207-9a43-b4bf89f76b47", 23 | "type": "application-startup-healthcheck", 24 | "attributes": { 25 | "postgres": true, 26 | "redis": true, 27 | "rabbit": true 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ## [0.2.0] - 2024-03-28 34 | 35 | ### Added 36 | 37 | - Added ability to use configuration with default settings without block passing 38 | 39 | ```ruby 40 | HealthcheckEndpoint.configure # It will create configuration instance with default settings 41 | ``` 42 | 43 | ### Updated 44 | 45 | - Updated `HealthcheckEndpoint.configure`, tests 46 | - Updated gem documentation 47 | 48 | ## [0.1.0] - 2024-03-26 49 | 50 | ### Added 51 | 52 | - First release of `healthcheck_endpoint`. 53 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention 26 | or advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in 32 | a professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `healthcheck_endpoint` 2 | 3 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. 6 | 7 | ## Using the issue tracker 8 | 9 | The issue tracker is the preferred channel for [issue/bug reports](#issuebug-reports), [feature requests](#feature-requests), [questions](#questions) and submitting [pull requests](#pull-requests). 10 | 11 | ## Issue/bug reports 12 | 13 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful - thank you! 14 | 15 | Guidelines for issue/bug reports: 16 | 17 | 1. **Use the GitHub issue search** — check if the issue has already been reported 18 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or `develop` branch in the repository 19 | 3. `healthcheck_endpoint` [issue template](.github/ISSUE_TEMPLATE/issue_report.md)/[bug template](.github/ISSUE_TEMPLATE/bug_report.md) 20 | 21 | A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 22 | 23 | ## Feature requests 24 | 25 | Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to _you_ to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. 26 | 27 | ## Questions 28 | 29 | We're always open to a new conversations. So if you have any questions just ask us. 30 | 31 | ## Pull requests 32 | 33 | Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. 34 | 35 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. 36 | 37 | Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). Not all features proposed will be added but we are open to having a conversation about a feature you are championing. 38 | 39 | Guidelines for pull requests: 40 | 41 | 1. `healthcheck_endpoint` [pull request template](.github/PULL_REQUEST_TEMPLATE.md) 42 | 2. Fork the repo, checkout to `develop` branch 43 | 3. Run the tests. This is to make sure your starting point works 44 | 4. Read our [branch naming convention](.github/BRANCH_NAMING_CONVENTION.md) 45 | 5. Create a new branch 46 | 6. Read our [setup development environment guide](.github/DEVELOPMENT_ENVIRONMENT_GUIDE.md) 47 | 7. Make your changes. Please note that your PR should include tests for the new codebase! 48 | 8. Push to your fork and submit a pull request to `develop` branch 49 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Vladislav Trotsenko 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 | # ![HealthcheckEndpoint - Simple configurable application healthcheck rack middleware](https://repository-images.githubusercontent.com/769335579/012755ac-8757-43b1-8191-7cac167396fa) 2 | 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/e1da0941cf2234c1d110/maintainability)](https://codeclimate.com/github/obstools/healthcheck-endpoint/maintainability) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/e1da0941cf2234c1d110/test_coverage)](https://codeclimate.com/github/obstools/healthcheck-endpoint/test_coverage) 5 | [![CircleCI](https://circleci.com/gh/obstools/healthcheck-endpoint/tree/master.svg?style=svg)](https://circleci.com/gh/obstools/healthcheck-endpoint/tree/master) 6 | [![Gem Version](https://badge.fury.io/rb/healthcheck_endpoint.svg)](https://badge.fury.io/rb/healthcheck_endpoint) 7 | [![Downloads](https://img.shields.io/gem/dt/healthcheck_endpoint.svg?colorA=004d99&colorB=0073e6)](https://rubygems.org/gems/healthcheck_endpoint) 8 | [![GitHub](https://img.shields.io/github/license/obstools/healthcheck-endpoint)](LICENSE.txt) 9 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) 10 | 11 | Simple configurable application healthcheck rack middleware. This middleware allows you to embed healthcheck endpoints into your rack based application to perform healthcheck probes. Make your application compatible with [Docker](https://docs.docker.com/reference/dockerfile/#healthcheck)/[Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request) healthchecks in a seconds. 12 | 13 | ## Table of Contents 14 | 15 | - [Features](#features) 16 | - [Requirements](#requirements) 17 | - [Installation](#installation) 18 | - [Configuring](#configuring) 19 | - [Usage](#usage) 20 | - [Integration](#integration) 21 | - [Rack](#rack) 22 | - [Roda](#roda) 23 | - [Hanami](#hanami) 24 | - [Rails](#rails) 25 | - [Healthcheck endpoint response](#healthcheck-endpoint-response) 26 | - [Contributing](#contributing) 27 | - [License](#license) 28 | - [Code of Conduct](#code-of-conduct) 29 | - [Credits](#credits) 30 | - [Versioning](#versioning) 31 | - [Changelog](CHANGELOG.md) 32 | 33 | ## Features 34 | 35 | - Built-in default configuration 36 | - Configurable services for startup/liveness/readiness probes 37 | - Configurable root endpoints namespace 38 | - Configurable startup/liveness/readiness probes endpoints 39 | - Configurable successful/failure response statuses 40 | 41 | ## Requirements 42 | 43 | Ruby MRI 2.5.0+ 44 | 45 | ## Installation 46 | 47 | Add this line to your application's Gemfile: 48 | 49 | ```ruby 50 | gem 'healthcheck_endpoint' 51 | ``` 52 | 53 | And then execute: 54 | 55 | ```bash 56 | bundle 57 | ``` 58 | 59 | Or install it yourself as: 60 | 61 | ```bash 62 | gem install healthcheck_endpoint 63 | ``` 64 | 65 | ## Configuring 66 | 67 | To start working with this gem, you must configure it first as in the example below: 68 | 69 | ```ruby 70 | # config/initializers/healthcheck_endpoint.rb 71 | 72 | require 'healthcheck_endpoint' 73 | 74 | HealthcheckEndpoint.configure do |config| 75 | # Optional parameter. The list of services that can be triggered 76 | # during running probes. Each value of this hash should be callable 77 | # and return boolean. 78 | # It is equal to empty hash by default. 79 | config.services = { 80 | postgres: -> { true }, 81 | redis: -> { true }, 82 | rabbit: -> { false } 83 | } 84 | 85 | # Optional parameter. The list of services that will be checked 86 | # during running startup probe. As array items must be used an 87 | # existing keys, defined in config.services. 88 | # It is equal to empty array by default. 89 | config.services_startup = %i[postgres] 90 | 91 | # Optional parameter. The list of services that will be checked 92 | # during running liveness probe. As array items must be used an 93 | # existing keys, defined in config.services. 94 | # It is equal to empty array by default. 95 | config.services_liveness = %i[redis] 96 | 97 | # Optional parameter. The list of services that will be checked 98 | # during running liveness probe. As array items must be used an 99 | # existing keys, defined in config.services. 100 | # It is equal to empty array by default. 101 | config.services_readiness = %i[postgres redis rabbit] 102 | 103 | # Optional parameter. The name of middleware's root 104 | # endpoints namespace. Use '/' if you want to use root 105 | # namespace. It is equal to /healthcheck by default. 106 | config.endpoints_namespace = '/application-healthcheck' 107 | 108 | # Optional parameter. The startup endpoint path. 109 | # It is equal to /startup by default. 110 | config.endpoint_startup = '/startup-probe' 111 | 112 | # Optional parameter. The liveness endpoint path. 113 | # It is equal to /liveness by default. 114 | config.endpoint_liveness = '/liveness-probe' 115 | 116 | # Optional parameter. The readiness endpoint path. 117 | # It is equal to /readiness by default. 118 | config.endpoint_readiness = '/readiness-probe' 119 | 120 | # Optional parameter. The HTTP successful status 121 | # for startup probe. It is equal to 200 by default. 122 | config.endpoint_startup_status_success = 201 123 | 124 | # Optional parameter. The HTTP successful status 125 | # for liveness probe. It is equal to 200 by default. 126 | config.endpoint_liveness_status_success = 202 127 | 128 | # Optional parameter. The HTTP successful status 129 | # for readiness probe. It is equal to 200 by default. 130 | config.endpoint_readiness_status_success = 203 131 | 132 | # Optional parameter. The HTTP failure status 133 | # for startup probe. It is equal to 500 by default. 134 | config.endpoint_startup_status_failure = 501 135 | 136 | # Optional parameter. The HTTP failure status 137 | # for liveness probe. It is equal to 500 by default. 138 | config.endpoint_liveness_status_failure = 502 139 | 140 | # Optional parameter. The HTTP failure status 141 | # for readiness probe. It is equal to 500 by default. 142 | config.endpoint_readiness_status_failure = 503 143 | end 144 | ``` 145 | 146 | ## Usage 147 | 148 | ### Integration 149 | 150 | Please note, to start using this middleware you should configure `HealthcheckEndpoint` before and then you should to add `HealthcheckEndpoint::RackMiddleware` on the top of middlewares list. 151 | 152 | #### Rack 153 | 154 | ```ruby 155 | require 'healthcheck_endpoint' 156 | 157 | # Configuring HealthcheckEndpoint with default settings 158 | HealthcheckEndpoint.configure 159 | 160 | Rack::Builder.app do 161 | use HealthcheckEndpoint::RackMiddleware 162 | run YourApplication 163 | end 164 | ``` 165 | 166 | #### Roda 167 | 168 | ```ruby 169 | require 'healthcheck_endpoint' 170 | 171 | # Configuring HealthcheckEndpoint with default settings 172 | HealthcheckEndpoint.configure 173 | 174 | class YourApplication < Roda 175 | use HealthcheckEndpoint::RackMiddleware 176 | end 177 | ``` 178 | 179 | #### Hanami 180 | 181 | ```ruby 182 | # config/initializers/healthcheck_endpoint.rb 183 | 184 | require 'healthcheck_endpoint' 185 | 186 | # Configuring HealthcheckEndpoint with default settings 187 | HealthcheckEndpoint.configure 188 | ``` 189 | 190 | ```ruby 191 | # config/environment.rb 192 | 193 | Hanami.configure do 194 | middleware.use HealthcheckEndpoint::RackMiddleware 195 | end 196 | ``` 197 | 198 | #### Rails 199 | 200 | For Rails 7+, you can use the built-in healthcheck feature provided by Rails itself. See the [official Rails documentation on healthchecks](https://edgeapi.rubyonrails.org/classes/Rails/HealthController.html) for details. However, if you need more advanced healthcheck functionality or want to maintain consistency across different frameworks, you can still use this gem with Rails as follows: 201 | 202 | ```ruby 203 | # config/initializers/healthcheck_endpoint.rb 204 | 205 | require 'healthcheck_endpoint' 206 | 207 | # Configuring HealthcheckEndpoint with default settings 208 | HealthcheckEndpoint.configure 209 | ``` 210 | 211 | ```ruby 212 | # config/application.rb 213 | 214 | class Application < Rails::Application 215 | config.middleware.use HealthcheckEndpoint::RackMiddleware 216 | end 217 | ``` 218 | 219 | ### Healthcheck endpoint response 220 | 221 | Each healthcheck endpoint returns proper HTTP status and body. Determining the response status is based on the general result of service checks (when all are successful the status is successful, at least one failure - the status is failure). The response body represented as JSON document with a structure like in the example below: 222 | 223 | ```json 224 | { 225 | "data": { 226 | "id": "a09efd18-e09f-4207-9a43-b4bf89f76b47", 227 | "type": "application-startup-healthcheck", 228 | "attributes": { 229 | "postgres": true, 230 | "redis": true, 231 | "rabbit": true 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | ## Contributing 238 | 239 | Bug reports and pull requests are welcome on GitHub at . This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. Please check the [open tickets](https://github.com/obstools/healthcheck-endpoint/issues). Be sure to follow Contributor Code of Conduct below and our [Contributing Guidelines](CONTRIBUTING.md). 240 | 241 | ## License 242 | 243 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 244 | 245 | ## Code of Conduct 246 | 247 | Everyone interacting in the `healthcheck_endpoint` project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md). 248 | 249 | ## Credits 250 | 251 | - [The Contributors](https://github.com/obstools/healthcheck-endpoint/graphs/contributors) for code and awesome suggestions 252 | - [The Stargazers](https://github.com/obstools/healthcheck-endpoint/stargazers) for showing their support 253 | 254 | ## Versioning 255 | 256 | `healthcheck_endpoint` uses [Semantic Versioning 2.0.0](https://semver.org) 257 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'healthcheck_endpoint' 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 | -------------------------------------------------------------------------------- /healthcheck_endpoint.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/healthcheck_endpoint/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'healthcheck_endpoint' 7 | spec.version = HealthcheckEndpoint::VERSION 8 | spec.authors = ['Vladislav Trotsenko'] 9 | spec.email = %w[admin@bestweb.com.ua] 10 | spec.summary = %(Simple configurable application healthcheck rack middleware) 11 | spec.description = %(Simple configurable application healthcheck rack middleware.) 12 | spec.homepage = 'https://github.com/obstools/healthcheck-endpoint' 13 | spec.license = 'MIT' 14 | 15 | current_ruby_version = ::Gem::Version.new(::RUBY_VERSION) 16 | ffaker_version = current_ruby_version >= ::Gem::Version.new('3.0.0') ? '~> 2.23' : '~> 2.21' 17 | 18 | spec.required_ruby_version = '>= 2.5.0' 19 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 20 | spec.require_paths = %w[lib] 21 | 22 | spec.add_runtime_dependency 'rack', '>= 2.0.1' 23 | 24 | spec.add_development_dependency 'ffaker', ffaker_version 25 | spec.add_development_dependency 'json_matchers', '~> 0.11.1' 26 | spec.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1' 27 | spec.add_development_dependency 'rspec', '~> 3.13' 28 | end 29 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'healthcheck_endpoint/core' 4 | 5 | module HealthcheckEndpoint 6 | class << self 7 | def configuration(&block) 8 | @configuration ||= begin 9 | return unless block 10 | 11 | HealthcheckEndpoint::Configuration.new(&block) 12 | end 13 | end 14 | 15 | def configure(&block) 16 | return configuration {} unless block # rubocop:disable Lint/EmptyBlock 17 | 18 | configuration(&block) 19 | end 20 | 21 | def reset_configuration! 22 | @configuration = nil 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | class Configuration 5 | ATTRIBUTES = %i[ 6 | services 7 | services_startup 8 | services_liveness 9 | services_readiness 10 | endpoints_namespace 11 | endpoint_startup 12 | endpoint_liveness 13 | endpoint_readiness 14 | endpoint_startup_status_success 15 | endpoint_liveness_status_success 16 | endpoint_readiness_status_success 17 | endpoint_startup_status_failure 18 | endpoint_liveness_status_failure 19 | endpoint_readiness_status_failure 20 | ].freeze 21 | ENDPOINTS_NAMESPACE = '/healthcheck' 22 | ENDPOINT_STARTUP = '/startup' 23 | ENDPOINT_LIVENESS = '/liveness' 24 | ENDPOINT_READINESS = '/readiness' 25 | DEFAULT_HTTP_STATUS_SUCCESS = 200 26 | DEFAULT_HTTP_STATUS_FAILURE = 500 27 | AVILABLE_HTTP_STATUSES_SUCCESS = (DEFAULT_HTTP_STATUS_SUCCESS..226).freeze 28 | AVILABLE_HTTP_STATUSES_FAILURE = (DEFAULT_HTTP_STATUS_FAILURE..511).freeze 29 | 30 | Settings = ::Struct.new(*HealthcheckEndpoint::Configuration::ATTRIBUTES, keyword_init: true) do 31 | def update(&block) 32 | return self unless block 33 | 34 | tap(&block) 35 | end 36 | end 37 | 38 | attr_reader(*HealthcheckEndpoint::Configuration::ATTRIBUTES) 39 | 40 | def initialize(&block) 41 | configuration_settings = build_configuration_settings(&block) 42 | HealthcheckEndpoint::Configuration::ATTRIBUTES.each do |attribute| 43 | public_send(:"#{attribute}=", configuration_settings.public_send(attribute)) 44 | end 45 | end 46 | 47 | HealthcheckEndpoint::Configuration::ATTRIBUTES.each do |attribute| 48 | define_method(:"#{attribute}=") do |argument| 49 | validate_attribute(__method__, attribute, argument) 50 | instance_variable_set(:"@#{attribute}", argument) 51 | end 52 | end 53 | 54 | private 55 | 56 | def build_configuration_settings(&block) # rubocop:disable Metrics/MethodLength 57 | HealthcheckEndpoint::Configuration::Settings.new( 58 | services: {}, 59 | services_startup: [], 60 | services_liveness: [], 61 | services_readiness: [], 62 | endpoints_namespace: HealthcheckEndpoint::Configuration::ENDPOINTS_NAMESPACE, 63 | endpoint_startup: HealthcheckEndpoint::Configuration::ENDPOINT_STARTUP, 64 | endpoint_liveness: HealthcheckEndpoint::Configuration::ENDPOINT_LIVENESS, 65 | endpoint_readiness: HealthcheckEndpoint::Configuration::ENDPOINT_READINESS, 66 | endpoint_startup_status_success: HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_SUCCESS, 67 | endpoint_liveness_status_success: HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_SUCCESS, 68 | endpoint_readiness_status_success: HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_SUCCESS, 69 | endpoint_startup_status_failure: HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_FAILURE, 70 | endpoint_liveness_status_failure: HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_FAILURE, 71 | endpoint_readiness_status_failure: HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_FAILURE 72 | ).update(&block) 73 | end 74 | 75 | def validate_attribute(method, attribute, value) # rubocop:disable Metrics/AbcSize 76 | raise_unless(HealthcheckEndpoint::Error::Configuration::ArgumentType, method, *validator_argument_type(attribute, value)) 77 | case attribute 78 | when HealthcheckEndpoint::Configuration::ATTRIBUTES[0] 79 | raise_unless(HealthcheckEndpoint::Error::Configuration::NotCallableService, method, *validator_services_callable(value)) 80 | when *HealthcheckEndpoint::Configuration::ATTRIBUTES[1..3] 81 | raise_unless(HealthcheckEndpoint::Error::Configuration::UnknownService, method, *validator_services_conformity(value)) 82 | when *HealthcheckEndpoint::Configuration::ATTRIBUTES[4..7] 83 | raise_unless(HealthcheckEndpoint::Error::Configuration::EnpointPattern, method, *validator_endpoint(value)) 84 | when *HealthcheckEndpoint::Configuration::ATTRIBUTES[8..10] 85 | raise_unless(HealthcheckEndpoint::Error::Configuration::HttpStatusSuccess, method, *validator_http_status_success(value)) 86 | when *HealthcheckEndpoint::Configuration::ATTRIBUTES[11..13] 87 | raise_unless(HealthcheckEndpoint::Error::Configuration::HttpStatusFailure, method, *validator_http_status_failure(value)) 88 | end 89 | end 90 | 91 | def validator_argument_type(method_name, argument) 92 | [ 93 | argument, 94 | argument.is_a?( 95 | case method_name 96 | when :services then ::Hash 97 | when *HealthcheckEndpoint::Configuration::ATTRIBUTES[1..3] then ::Array 98 | when *HealthcheckEndpoint::Configuration::ATTRIBUTES[4..7] then ::String 99 | when *HealthcheckEndpoint::Configuration::ATTRIBUTES[8..13] then ::Integer 100 | end 101 | ) 102 | ] 103 | end 104 | 105 | def validator_endpoint(argument) 106 | [argument, argument[%r{\A/.*\z}]] 107 | end 108 | 109 | def validator_http_status_success(argument) 110 | [argument, HealthcheckEndpoint::Configuration::AVILABLE_HTTP_STATUSES_SUCCESS.include?(argument)] 111 | end 112 | 113 | def validator_http_status_failure(argument) 114 | [argument, HealthcheckEndpoint::Configuration::AVILABLE_HTTP_STATUSES_FAILURE.include?(argument)] 115 | end 116 | 117 | def validator_services_callable(services_to_check, target_service = nil) 118 | result = services_to_check.all? do |service_name, service_context| 119 | target_service = service_name 120 | service_context.respond_to?(:call) 121 | end 122 | 123 | [target_service, result] 124 | end 125 | 126 | def validator_services_conformity(services_to_check, target_service = nil) 127 | result = services_to_check.all? do |service_name| 128 | target_service = service_name 129 | services.key?(service_name) 130 | end 131 | 132 | [target_service, result] 133 | end 134 | 135 | def raise_unless(exception_class, argument_name, argument_context, condition) 136 | raise exception_class.new(argument_context, argument_name) unless condition 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | require_relative 'error/configuration/argument_type' 7 | require_relative 'error/configuration/unknown_service' 8 | require_relative 'error/configuration/not_callable_service' 9 | require_relative 'error/configuration/enpoint_pattern' 10 | require_relative 'error/configuration/http_status_success' 11 | require_relative 'error/configuration/http_status_failure' 12 | require_relative 'error/configuration/not_configured' 13 | end 14 | end 15 | 16 | require_relative 'version' 17 | require_relative 'configuration' 18 | require_relative 'resolver' 19 | require_relative 'rack_middleware' 20 | end 21 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/error/configuration/argument_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | ArgumentType = ::Class.new(::ArgumentError) do 7 | def initialize(arg_value, arg_name) 8 | super("#{arg_value} is not a valid #{arg_name}") 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/error/configuration/enpoint_pattern.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | EnpointPattern = ::Class.new(::ArgumentError) do 7 | def initialize(arg_value, arg_name) 8 | super("#{arg_value} does not match a valid enpoint pattern for #{arg_name}") 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/error/configuration/http_status_failure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | HttpStatusFailure = ::Class.new(::ArgumentError) do 7 | def initialize(arg_value, arg_name) 8 | super("Status #{arg_value} is wrong HTTP failure status for #{arg_name}. It should be in the range 500-511") 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/error/configuration/http_status_success.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | HttpStatusSuccess = ::Class.new(::ArgumentError) do 7 | def initialize(arg_value, arg_name) 8 | super("Status #{arg_value} is wrong HTTP successful status for #{arg_name}. It should be in the range 200-226") 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/error/configuration/not_callable_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | NotCallableService = ::Class.new(::ArgumentError) do 7 | def initialize(service_name, services_setter) 8 | super("Service #{service_name} is not callable. All values for #{services_setter} should be a callable objects") 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/error/configuration/not_configured.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | NotConfigured = ::Class.new(::RuntimeError) do 7 | def initialize 8 | super('The configuration is empty. Please use HealthcheckEndpoint.configure before') 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/error/configuration/unknown_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module Error 5 | module Configuration 6 | UnknownService = ::Class.new(::ArgumentError) do 7 | def initialize(service_name, services_setter) 8 | super("Unknown #{service_name} service name for #{services_setter}. You should define it in config.services firstly") 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/rack_middleware.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | class RackMiddleware 5 | def initialize( 6 | app, 7 | resolver = HealthcheckEndpoint::Resolver, 8 | counfigured = !!HealthcheckEndpoint.configuration 9 | ) 10 | @app = app 11 | @resolver = resolver 12 | @counfigured = counfigured 13 | end 14 | 15 | def call(env) 16 | raise HealthcheckEndpoint::Error::Configuration::NotConfigured unless counfigured 17 | 18 | resolver.call(env) || app.call(env) 19 | end 20 | 21 | private 22 | 23 | attr_reader :app, :resolver, :counfigured 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | class Resolver 5 | require 'rack' 6 | require 'securerandom' 7 | require 'json' 8 | 9 | PROBE_ENDPOINTS = %i[endpoint_startup endpoint_liveness endpoint_readiness].freeze 10 | CONTENT_TYPE = { 'Content-Type' => 'application/json' }.freeze 11 | ROOT_NAMESPACE = '/' 12 | 13 | def self.call(rack_env) 14 | new(rack_env).call 15 | end 16 | 17 | def initialize(rack_env) 18 | @request = ::Rack::Request.new(rack_env) 19 | end 20 | 21 | def call 22 | return unless probe_name 23 | 24 | [response_status, HealthcheckEndpoint::Resolver::CONTENT_TYPE, [response_jsonapi]] 25 | end 26 | 27 | private 28 | 29 | attr_reader :request 30 | 31 | def configuration 32 | HealthcheckEndpoint.configuration 33 | end 34 | 35 | def root_namespace 36 | @root_namespace ||= configuration.endpoints_namespace 37 | end 38 | 39 | def root_namespace? 40 | root_namespace.eql?(HealthcheckEndpoint::Resolver::ROOT_NAMESPACE) 41 | end 42 | 43 | HealthcheckEndpoint::Resolver::PROBE_ENDPOINTS.each do |method| 44 | define_method(method) do 45 | target_endpoint = configuration.public_send(method) 46 | root_namespace? ? target_endpoint : "#{root_namespace}#{target_endpoint}" 47 | end 48 | end 49 | 50 | def probe_name 51 | @probe_name ||= 52 | case request.path 53 | when endpoint_startup then :startup 54 | when endpoint_liveness then :liveness 55 | when endpoint_readiness then :readiness 56 | end 57 | end 58 | 59 | def probe_result 60 | @probe_result ||= 61 | configuration.public_send(:"services_#{probe_name}").each_with_object({}) do |service_name, response| 62 | response[service_name] = configuration.services[service_name].call 63 | end 64 | end 65 | 66 | def response_status 67 | configuration.public_send( 68 | probe_result.values.all? ? :"endpoint_#{probe_name}_status_success" : :"endpoint_#{probe_name}_status_failure" 69 | ) 70 | end 71 | 72 | def response_jsonapi 73 | { 74 | data: { 75 | id: ::SecureRandom.uuid, 76 | type: "application-#{probe_name}-healthcheck", # TODO: it would be great to be able to configure this 77 | attributes: probe_result 78 | } 79 | }.to_json 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/healthcheck_endpoint/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | VERSION = '1.0.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Configuration do 4 | describe 'defined constants' do 5 | it { expect(described_class).to be_const_defined(:ATTRIBUTES) } 6 | it { expect(described_class).to be_const_defined(:ENDPOINTS_NAMESPACE) } 7 | it { expect(described_class).to be_const_defined(:ENDPOINT_STARTUP) } 8 | it { expect(described_class).to be_const_defined(:ENDPOINT_LIVENESS) } 9 | it { expect(described_class).to be_const_defined(:ENDPOINT_READINESS) } 10 | it { expect(described_class).to be_const_defined(:DEFAULT_HTTP_STATUS_SUCCESS) } 11 | it { expect(described_class).to be_const_defined(:DEFAULT_HTTP_STATUS_FAILURE) } 12 | it { expect(described_class).to be_const_defined(:AVILABLE_HTTP_STATUSES_SUCCESS) } 13 | it { expect(described_class).to be_const_defined(:AVILABLE_HTTP_STATUSES_FAILURE) } 14 | end 15 | 16 | describe HealthcheckEndpoint::Configuration::Settings do 17 | describe '#update' do 18 | subject(:updated_settings) { described_class.new.update(&block) } 19 | 20 | context 'when configuration block has been passed' do 21 | let(:block) { ->(config) { config.services = 42 } } 22 | 23 | it 'updates structure' do 24 | expect(updated_settings).to be_an_instance_of(described_class) 25 | expect(updated_settings.services).to eq(42) 26 | end 27 | end 28 | 29 | context 'when configuration block has not been passed' do 30 | let(:block) { nil } 31 | 32 | it 'does not update structure' do 33 | expect(updated_settings).to be_an_instance_of(described_class) 34 | expect(updated_settings.services).to be_nil 35 | end 36 | end 37 | end 38 | end 39 | 40 | describe '.new' do 41 | subject(:configuration) do 42 | create_configuration( 43 | services: services, 44 | services_startup: services_startup, 45 | services_liveness: services_liveness, 46 | services_readiness: services_readiness, 47 | endpoints_namespace: endpoints_namespace, 48 | endpoint_startup: endpoint_startup, 49 | endpoint_liveness: endpoint_liveness, 50 | endpoint_readiness: endpoint_readiness, 51 | endpoint_startup_status_success: endpoint_startup_status_success, 52 | endpoint_liveness_status_success: endpoint_liveness_status_success, 53 | endpoint_readiness_status_success: endpoint_readiness_status_success, 54 | endpoint_startup_status_failure: endpoint_startup_status_failure, 55 | endpoint_liveness_status_failure: endpoint_liveness_status_failure, 56 | endpoint_readiness_status_failure: endpoint_readiness_status_failure 57 | ) 58 | end 59 | 60 | context 'when valid configuration' do 61 | let(:service_name) { create_service(random_service_name) } 62 | let(:services) { create_service(service_name) } 63 | let(:services_startup) { [service_name] } 64 | let(:services_liveness) { [service_name] } 65 | let(:services_readiness) { [service_name] } 66 | let(:endpoints_namespace) { random_endpoint } 67 | let(:endpoint_startup) { random_endpoint } 68 | let(:endpoint_liveness) { random_endpoint } 69 | let(:endpoint_readiness) { random_endpoint } 70 | let(:endpoint_startup_status_success) { random_http_status(successful: true) } 71 | let(:endpoint_liveness_status_success) { random_http_status(successful: true) } 72 | let(:endpoint_readiness_status_success) { random_http_status(successful: true) } 73 | let(:endpoint_startup_status_failure) { random_http_status(successful: false) } 74 | let(:endpoint_liveness_status_failure) { random_http_status(successful: false) } 75 | let(:endpoint_readiness_status_failure) { random_http_status(successful: false) } 76 | 77 | it 'creates configuration instance' do 78 | expect(configuration.services).to eq(services) 79 | expect(configuration.services_startup).to eq(services_startup) 80 | expect(configuration.services_liveness).to eq(services_liveness) 81 | expect(configuration.services_readiness).to eq(services_readiness) 82 | expect(configuration.endpoints_namespace).to eq(endpoints_namespace) 83 | expect(configuration.endpoint_startup).to eq(endpoint_startup) 84 | expect(configuration.endpoint_liveness).to eq(endpoint_liveness) 85 | expect(configuration.endpoint_readiness).to eq(endpoint_readiness) 86 | expect(configuration.endpoint_startup_status_success).to eq(endpoint_startup_status_success) 87 | expect(configuration.endpoint_liveness_status_success).to eq(endpoint_liveness_status_success) 88 | expect(configuration.endpoint_readiness_status_success).to eq(endpoint_readiness_status_success) 89 | expect(configuration.endpoint_startup_status_failure).to eq(endpoint_startup_status_failure) 90 | expect(configuration.endpoint_liveness_status_failure).to eq(endpoint_liveness_status_failure) 91 | expect(configuration.endpoint_readiness_status_failure).to eq(endpoint_readiness_status_failure) 92 | expect(configuration).to be_an_instance_of(described_class) 93 | end 94 | end 95 | 96 | context 'when invalid configuration' do 97 | shared_examples 'raies argument error' do 98 | it 'raies argument error' do 99 | expect { configuration }.to raise_error( 100 | target_exception_class, 101 | expected_error_message 102 | ) 103 | end 104 | end 105 | 106 | let(:invalid_argument) { 42 } 107 | 108 | context 'when services= setter is invalid' do 109 | context 'when argument has wrong type' do 110 | subject(:configuration) do 111 | create_configuration(services: invalid_argument) 112 | end 113 | 114 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::ArgumentType } 115 | let(:expected_error_message) { "#{invalid_argument} is not a valid services=" } 116 | 117 | include_examples 'raies argument error' 118 | end 119 | 120 | context 'when argument nested value is not a callable object' do 121 | subject(:configuration) do 122 | create_configuration(services: { service_name => invalid_argument }) 123 | end 124 | 125 | let(:service_name) { random_service_name } 126 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::NotCallableService } 127 | let(:expected_error_message) do 128 | "Service #{service_name} is not callable. All values for services= should be a callable objects" 129 | end 130 | 131 | include_examples 'raies argument error' 132 | end 133 | end 134 | 135 | context 'when services_startup= setter is invalid' do 136 | context 'when argument has wrong type' do 137 | subject(:configuration) do 138 | create_configuration(services_startup: invalid_argument) 139 | end 140 | 141 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::ArgumentType } 142 | let(:expected_error_message) { "#{invalid_argument} is not a valid services_startup=" } 143 | 144 | include_examples 'raies argument error' 145 | end 146 | 147 | context 'when argument nested value is not a defined service' do 148 | subject(:configuration) do 149 | create_configuration(services_startup: [invalid_argument]) 150 | end 151 | 152 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::UnknownService } 153 | let(:expected_error_message) do 154 | "Unknown #{invalid_argument} service name for services_startup=. You should define it in config.services firstly" 155 | end 156 | 157 | include_examples 'raies argument error' 158 | end 159 | end 160 | 161 | context 'when services_liveness= setter is invalid' do 162 | context 'when argument has wrong type' do 163 | subject(:configuration) do 164 | create_configuration(services_liveness: invalid_argument) 165 | end 166 | 167 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::ArgumentType } 168 | let(:expected_error_message) { "#{invalid_argument} is not a valid services_liveness=" } 169 | 170 | include_examples 'raies argument error' 171 | end 172 | 173 | context 'when argument nested value is not a defined service' do 174 | subject(:configuration) do 175 | create_configuration(services_liveness: [invalid_argument]) 176 | end 177 | 178 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::UnknownService } 179 | let(:expected_error_message) do 180 | "Unknown #{invalid_argument} service name for services_liveness=. You should define it in config.services firstly" 181 | end 182 | 183 | include_examples 'raies argument error' 184 | end 185 | end 186 | 187 | context 'when services_readiness= setter is invalid' do 188 | context 'when argument has wrong type' do 189 | subject(:configuration) do 190 | create_configuration(services_readiness: invalid_argument) 191 | end 192 | 193 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::ArgumentType } 194 | let(:expected_error_message) { "#{invalid_argument} is not a valid services_readiness=" } 195 | 196 | include_examples 'raies argument error' 197 | end 198 | 199 | context 'when argument nested value is not a defined service' do 200 | subject(:configuration) do 201 | create_configuration(services_readiness: [invalid_argument]) 202 | end 203 | 204 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::UnknownService } 205 | let(:expected_error_message) do 206 | "Unknown #{invalid_argument} service name for services_readiness=. You should define it in config.services firstly" 207 | end 208 | 209 | include_examples 'raies argument error' 210 | end 211 | end 212 | 213 | HealthcheckEndpoint::Configuration::ATTRIBUTES[4..7].each do |endpoint_setter| 214 | context "when #{endpoint_setter}= setter is invalid" do 215 | context 'when argument has wrong type' do 216 | subject(:configuration) do 217 | create_configuration(endpoint_setter => invalid_argument) 218 | end 219 | 220 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::ArgumentType } 221 | let(:expected_error_message) { "#{invalid_argument} is not a valid #{endpoint_setter}=" } 222 | 223 | include_examples 'raies argument error' 224 | end 225 | 226 | context 'when argument is not match to enpoint pattern' do 227 | subject(:configuration) do 228 | create_configuration(endpoint_setter => endpoint) 229 | end 230 | 231 | let(:endpoint) { invalid_argument.to_s } 232 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::EnpointPattern } 233 | let(:expected_error_message) do 234 | "#{endpoint} does not match a valid enpoint pattern for #{endpoint_setter}=" 235 | end 236 | 237 | include_examples 'raies argument error' 238 | end 239 | end 240 | end 241 | 242 | HealthcheckEndpoint::Configuration::ATTRIBUTES[8..10].each do |endpoint_setter| 243 | context "when #{endpoint_setter}= setter is invalid" do 244 | context 'when argument has wrong type' do 245 | subject(:configuration) do 246 | create_configuration(endpoint_setter => invalid_argument.to_s) 247 | end 248 | 249 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::ArgumentType } 250 | let(:expected_error_message) { "#{invalid_argument} is not a valid #{endpoint_setter}=" } 251 | 252 | include_examples 'raies argument error' 253 | end 254 | 255 | context 'when argument is not match to enpoint pattern' do 256 | subject(:configuration) do 257 | create_configuration(endpoint_setter => invalid_argument) 258 | end 259 | 260 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::HttpStatusSuccess } 261 | let(:expected_error_message) do 262 | "Status #{invalid_argument} is wrong HTTP successful status for #{endpoint_setter}=. It should be in the range 200-226" 263 | end 264 | 265 | include_examples 'raies argument error' 266 | end 267 | end 268 | end 269 | 270 | HealthcheckEndpoint::Configuration::ATTRIBUTES[11..13].each do |endpoint_setter| 271 | context "when #{endpoint_setter}= setter is invalid" do 272 | context 'when argument has wrong type' do 273 | subject(:configuration) do 274 | create_configuration(endpoint_setter => invalid_argument.to_s) 275 | end 276 | 277 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::ArgumentType } 278 | let(:expected_error_message) { "#{invalid_argument} is not a valid #{endpoint_setter}=" } 279 | 280 | include_examples 'raies argument error' 281 | end 282 | 283 | context 'when argument is not match to enpoint pattern' do 284 | subject(:configuration) do 285 | create_configuration(endpoint_setter => invalid_argument) 286 | end 287 | 288 | let(:target_exception_class) { HealthcheckEndpoint::Error::Configuration::HttpStatusFailure } 289 | let(:expected_error_message) do 290 | "Status #{invalid_argument} is wrong HTTP failure status for #{endpoint_setter}=. It should be in the range 500-511" 291 | end 292 | 293 | include_examples 'raies argument error' 294 | end 295 | end 296 | end 297 | end 298 | end 299 | end 300 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/error/configuration/argument_type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Error::Configuration::ArgumentType do 4 | subject(:error_instance) { described_class.new(arg_value, arg_name) } 5 | 6 | let(:arg_value) { random_message } 7 | let(:arg_name) { :some_arg_name } 8 | 9 | it { expect(described_class).to be < ::ArgumentError } 10 | it { expect(error_instance).to be_an_instance_of(described_class) } 11 | it { expect(error_instance.to_s).to eq("#{arg_value} is not a valid #{arg_name}") } 12 | end 13 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/error/configuration/http_status_failure_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Error::Configuration::HttpStatusFailure do 4 | subject(:error_instance) { described_class.new(arg_value, arg_name) } 5 | 6 | let(:arg_value) { random_http_status(successful: false) } 7 | let(:arg_name) { :some_arg_name } 8 | 9 | it { expect(described_class).to be < ::ArgumentError } 10 | it { expect(error_instance).to be_an_instance_of(described_class) } 11 | 12 | it 'returns exception message context' do 13 | expect(error_instance.to_s).to eq( 14 | "Status #{arg_value} is wrong HTTP failure status for #{arg_name}. It should be in the range 500-511" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/error/configuration/http_status_success_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Error::Configuration::HttpStatusSuccess do 4 | subject(:error_instance) { described_class.new(arg_value, arg_name) } 5 | 6 | let(:arg_value) { random_http_status(successful: false) } 7 | let(:arg_name) { :some_arg_name } 8 | 9 | it { expect(described_class).to be < ::ArgumentError } 10 | it { expect(error_instance).to be_an_instance_of(described_class) } 11 | 12 | it 'returns exception message context' do 13 | expect(error_instance.to_s).to eq( 14 | "Status #{arg_value} is wrong HTTP successful status for #{arg_name}. It should be in the range 200-226" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/error/configuration/not_callable_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Error::Configuration::NotCallableService do 4 | subject(:error_instance) { described_class.new(service_name, services_setter) } 5 | 6 | let(:service_name) { random_service_name } 7 | let(:services_setter) { :some_services_setter= } 8 | 9 | it { expect(described_class).to be < ::ArgumentError } 10 | it { expect(error_instance).to be_an_instance_of(described_class) } 11 | 12 | it 'returns exception message context' do 13 | expect(error_instance.to_s).to eq( 14 | "Service #{service_name} is not callable. All values for #{services_setter} should be a callable objects" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/error/configuration/not_configured_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Error::Configuration::NotConfigured do 4 | subject(:error_instance) { described_class.new } 5 | 6 | it { expect(described_class).to be < ::RuntimeError } 7 | it { expect(error_instance).to be_an_instance_of(described_class) } 8 | 9 | it 'returns exception message context' do 10 | expect(error_instance.to_s).to eq( 11 | 'The configuration is empty. Please use HealthcheckEndpoint.configure before' 12 | ) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/error/configuration/unknown_service_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Error::Configuration::UnknownService do 4 | subject(:error_instance) { described_class.new(service_name, services_setter) } 5 | 6 | let(:service_name) { random_service_name } 7 | let(:services_setter) { :some_services_setter= } 8 | 9 | it { expect(described_class).to be < ::ArgumentError } 10 | it { expect(error_instance).to be_an_instance_of(described_class) } 11 | 12 | it 'returns exception message context' do 13 | expect(error_instance.to_s).to eq( 14 | "Unknown #{service_name} service name for #{services_setter}. You should define it in config.services firstly" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/rack_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::RackMiddleware do 4 | subject(:middleware) { described_class.new(app, resolver).call(env) } 5 | 6 | let(:app_context) { 'some app rack response' } 7 | let(:app) { instance_double('RackApplication', call: app_context) } 8 | let(:resolver_context) { true } 9 | let(:resolver) { class_double('Resolver', call: resolver_context) } 10 | let(:env) { {} } 11 | 12 | context 'when HealthcheckEndpoint is configured' do 13 | before { init_configuration } 14 | 15 | context 'when resolver matches the route' do 16 | it 'returns rack response' do 17 | expect(resolver).to receive(:call).with(env) 18 | expect(middleware).to eq(resolver_context) 19 | end 20 | end 21 | 22 | context 'when resolver does not match the route' do 23 | let(:resolver_context) { nil } 24 | 25 | it 'returns rack response' do 26 | expect(resolver).to receive(:call).with(env) 27 | expect(app).to receive(:call).with(env) 28 | expect(middleware).to eq(app_context) 29 | end 30 | end 31 | end 32 | 33 | context 'when HealthcheckEndpoint is not configured' do 34 | it 'raises NotConfigured exception' do 35 | expect { middleware }.to raise_error( 36 | HealthcheckEndpoint::Error::Configuration::NotConfigured, 37 | 'The configuration is empty. Please use HealthcheckEndpoint.configure before' 38 | ) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/resolver_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::Resolver do 4 | describe 'defined constants' do 5 | it { expect(described_class).to be_const_defined(:PROBE_ENDPOINTS) } 6 | it { expect(described_class).to be_const_defined(:CONTENT_TYPE) } 7 | it { expect(described_class).to be_const_defined(:ROOT_NAMESPACE) } 8 | end 9 | 10 | describe '.call' do 11 | subject(:resolver) do 12 | described_class.call( 13 | Rack::MockRequest.env_for.merge('PATH_INFO' => healthcheck_endpoint) 14 | ) 15 | end 16 | 17 | let(:service_status_first) { true } 18 | let(:service_name_second) { random_service_name } 19 | let(:service_name_first) { random_service_name } 20 | 21 | context 'when unknown endpoint' do 22 | let(:healthcheck_endpoint) { '/unknown_endpoint' } 23 | 24 | before { init_configuration } 25 | 26 | it { is_expected.to be_nil } 27 | end 28 | 29 | %i[startup liveness readiness].each do |probe_name| 30 | context "when #{probe_name} root endpoint" do 31 | let(:service_status_second) { true } 32 | let(:healthcheck_endpoint) { current_configuration.public_send(:"endpoint_#{probe_name}") } 33 | 34 | before do 35 | init_configuration( 36 | services: { service_name_first => -> { service_status_first } }, 37 | "services_#{probe_name}": [service_name_first], 38 | endpoints_namespace: '/' 39 | ) 40 | end 41 | 42 | it 'returns rack middleware response' do 43 | response_status, _content_type, response_body = resolver 44 | 45 | expect(response_status).to eq( 46 | current_configuration.public_send(:"endpoint_#{probe_name}_status_success") 47 | ) 48 | expect(response_body.first).to include("application-#{probe_name}-healthcheck") 49 | end 50 | end 51 | 52 | context "when #{probe_name} endpoint" do 53 | [true, false].each do |status| # rubocop:disable Performance/CollectionLiteralInLoop 54 | context "when successful status is #{status}" do 55 | let(:service_status_second) { status } 56 | let(:healthcheck_endpoint) do 57 | endpoint = current_configuration.public_send(:"endpoint_#{probe_name}") 58 | "#{current_configuration.endpoints_namespace}#{endpoint}" 59 | end 60 | 61 | before do 62 | init_configuration( 63 | services: { 64 | service_name_first => -> { service_status_first }, 65 | service_name_second => -> { service_status_second } 66 | }, 67 | "services_#{probe_name}": [ 68 | service_name_first, 69 | service_name_second 70 | ] 71 | ) 72 | end 73 | 74 | it 'returns rack middleware response' do 75 | response_status, content_type, response = resolver 76 | response_body, target_status = response.first, "endpoint_#{probe_name}_status_#{status ? 'success' : 'failure'}" 77 | expected_probe_status = current_configuration.public_send(target_status) 78 | 79 | expect(response_status).to eq(expected_probe_status) 80 | expect(content_type).to eq('Content-Type' => 'application/json') 81 | expect(::JSON.parse(response_body).dig('data', 'attributes')).to eq( 82 | service_name_first => service_status_first, 83 | service_name_second => service_status_second 84 | ) 85 | expect(response_body).to match_json_schema('jsonapi_response') 86 | end 87 | end 88 | end 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/rspec_helper/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::RspecHelper::Configuration, type: :helper do 4 | describe '#configuration_block' do 5 | let(:configuration_params) { { param_one: 1, param_two: 2 } } 6 | let(:configuration_instance) { ::Struct.new(*configuration_params.keys).new } 7 | 8 | before { configuration_block(**configuration_params).call(configuration_instance) } 9 | 10 | it { expect(configuration_block).to be_an_instance_of(::Proc) } 11 | 12 | it 'sets configuration instance attributes' do 13 | configuration_params.each do |attribute, value| 14 | expect(configuration_instance.public_send(attribute)).to eq(value) 15 | end 16 | end 17 | end 18 | 19 | describe '#create_configuration' do 20 | subject(:configuration_builder) do 21 | create_configuration( 22 | services: services, 23 | services_startup: %i[a] 24 | ) 25 | end 26 | 27 | let(:services) { { a: -> {} } } 28 | let(:services_startup) { %i[a] } 29 | 30 | it 'returns configuration instance with defined params' do 31 | expect(configuration_builder).to be_an_instance_of(HealthcheckEndpoint::Configuration) 32 | expect(configuration_builder.services).to eq(services) 33 | expect(configuration_builder.services_startup).to eq(services_startup) 34 | end 35 | end 36 | 37 | describe '#init_configuration' do 38 | subject(:configuration_initializer) { init_configuration(endpoints_namespace: endpoints_namespace) } 39 | 40 | let(:endpoints_namespace) { '/some_endpoint' } 41 | 42 | it 'initializes configuration instance with random and predefined params' do 43 | expect(configuration_initializer).to be_an_instance_of(HealthcheckEndpoint::Configuration) 44 | expect(configuration_initializer.endpoints_namespace).to eq(endpoints_namespace) 45 | end 46 | end 47 | 48 | describe '#current_configuration' do 49 | subject(:current_configuration_instance) { current_configuration } 50 | 51 | it do 52 | expect(HealthcheckEndpoint).to receive(:configuration) 53 | current_configuration_instance 54 | end 55 | end 56 | 57 | describe '#reset_configuration' do 58 | subject(:reset_current_configuration) { reset_configuration } 59 | 60 | it do 61 | expect(HealthcheckEndpoint).to receive(:reset_configuration!) 62 | reset_current_configuration 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/rspec_helper/context_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::RspecHelper::ContextGenerator, type: :helper do 4 | describe '#random_message' do 5 | it 'returns random message' do 6 | expect(::FFaker::Lorem).to receive(:sentence).and_call_original 7 | expect(random_message).to be_an_instance_of(::String) 8 | end 9 | end 10 | 11 | describe '#random_service_name' do 12 | it 'returns random service name' do 13 | expect(::FFaker::InternetSE).to receive(:login_user_name).and_call_original 14 | expect(random_service_name).to be_an_instance_of(::String) 15 | end 16 | end 17 | 18 | describe '#random_endpoint' do 19 | it 'returns random endpoint' do 20 | expect(::FFaker::InternetSE).to receive(:login_user_name).and_call_original 21 | expect(random_endpoint).to match(%r{\A/.*\z}) 22 | end 23 | end 24 | 25 | describe '#random_http_status' do 26 | context 'when successful status' do 27 | it 'returns random successful http status' do 28 | expect(random_http_status(successful: true)).to be_between(200, 226) 29 | end 30 | end 31 | 32 | context 'when failure status' do 33 | it 'returns random failure http status' do 34 | expect(random_http_status(successful: false)).to be_between(500, 511) 35 | end 36 | end 37 | end 38 | 39 | describe '#create_service' do 40 | subject(:builded_service) { create_service(service_name, successful: service_type) } 41 | 42 | let(:service_name) { random_service_name } 43 | 44 | shared_examples 'returns hash with service' do 45 | it 'returns hash with service' do 46 | expect(builded_service.transform_values(&:call)).to eq(service_name => service_type) 47 | end 48 | end 49 | 50 | context 'when successful service' do 51 | let(:service_type) { true } 52 | 53 | include_examples 'returns hash with service' 54 | end 55 | 56 | context 'when failed service' do 57 | let(:service_type) { false } 58 | 59 | include_examples 'returns hash with service' 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint::VERSION do 4 | it { is_expected.not_to be_nil } 5 | end 6 | -------------------------------------------------------------------------------- /spec/healthcheck_endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe HealthcheckEndpoint do 4 | let(:service_name) { create_service(random_service_name) } 5 | let(:services) { create_service(service_name) } 6 | let(:services_startup) { [service_name] } 7 | let(:services_liveness) { [service_name] } 8 | let(:services_readiness) { [service_name] } 9 | let(:endpoints_namespace) { random_endpoint } 10 | let(:endpoint_startup) { random_endpoint } 11 | let(:endpoint_liveness) { random_endpoint } 12 | let(:endpoint_readiness) { random_endpoint } 13 | let(:endpoint_startup_status_success) { random_http_status(successful: true) } 14 | let(:endpoint_liveness_status_success) { random_http_status(successful: true) } 15 | let(:endpoint_readiness_status_success) { random_http_status(successful: true) } 16 | let(:endpoint_startup_status_failure) { random_http_status(successful: false) } 17 | let(:endpoint_liveness_status_failure) { random_http_status(successful: false) } 18 | let(:endpoint_readiness_status_failure) { random_http_status(successful: false) } 19 | 20 | describe '.configure' do 21 | subject(:configuration) { described_class.configure(&config_block) } 22 | 23 | let(:config_block) { nil } 24 | 25 | context 'without block' do 26 | it 'sets default attributes into configuration instance' do 27 | expect { configuration } 28 | .to change(described_class, :configuration) 29 | .from(nil) 30 | .to(be_instance_of(HealthcheckEndpoint::Configuration)) 31 | expect(configuration).to be_an_instance_of(HealthcheckEndpoint::Configuration) 32 | expect(configuration.services).to eq({}) 33 | expect(configuration.services_startup).to eq([]) 34 | expect(configuration.services_liveness).to eq([]) 35 | expect(configuration.services_readiness).to eq([]) 36 | expect(configuration.endpoints_namespace).to eq(HealthcheckEndpoint::Configuration::ENDPOINTS_NAMESPACE) 37 | expect(configuration.endpoint_startup).to eq(HealthcheckEndpoint::Configuration::ENDPOINT_STARTUP) 38 | expect(configuration.endpoint_liveness).to eq(HealthcheckEndpoint::Configuration::ENDPOINT_LIVENESS) 39 | expect(configuration.endpoint_readiness).to eq(HealthcheckEndpoint::Configuration::ENDPOINT_READINESS) 40 | expect(configuration.endpoint_startup_status_success).to eq(HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_SUCCESS) 41 | expect(configuration.endpoint_liveness_status_success).to eq(HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_SUCCESS) 42 | expect(configuration.endpoint_readiness_status_success).to eq(HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_SUCCESS) 43 | expect(configuration.endpoint_startup_status_failure).to eq(HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_FAILURE) 44 | expect(configuration.endpoint_liveness_status_failure).to eq(HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_FAILURE) 45 | expect(configuration.endpoint_readiness_status_failure).to eq(HealthcheckEndpoint::Configuration::DEFAULT_HTTP_STATUS_FAILURE) 46 | end 47 | end 48 | 49 | context 'with block' do 50 | let(:config_block) do 51 | configuration_block( 52 | services: services, 53 | services_startup: services_startup, 54 | services_liveness: services_liveness, 55 | services_readiness: services_readiness, 56 | endpoints_namespace: endpoints_namespace, 57 | endpoint_startup: endpoint_startup, 58 | endpoint_liveness: endpoint_liveness, 59 | endpoint_readiness: endpoint_readiness, 60 | endpoint_startup_status_success: endpoint_startup_status_success, 61 | endpoint_liveness_status_success: endpoint_liveness_status_success, 62 | endpoint_readiness_status_success: endpoint_readiness_status_success, 63 | endpoint_startup_status_failure: endpoint_startup_status_failure, 64 | endpoint_liveness_status_failure: endpoint_liveness_status_failure, 65 | endpoint_readiness_status_failure: endpoint_readiness_status_failure 66 | ) 67 | end 68 | 69 | it 'sets attributes into configuration instance' do 70 | expect { configuration } 71 | .to change(described_class, :configuration) 72 | .from(nil) 73 | .to(be_instance_of(HealthcheckEndpoint::Configuration)) 74 | expect(configuration).to be_an_instance_of(HealthcheckEndpoint::Configuration) 75 | expect(configuration.services).to eq(services) 76 | expect(configuration.services_startup).to eq(services_startup) 77 | expect(configuration.services_liveness).to eq(services_liveness) 78 | expect(configuration.services_readiness).to eq(services_readiness) 79 | expect(configuration.endpoints_namespace).to eq(endpoints_namespace) 80 | expect(configuration.endpoint_startup).to eq(endpoint_startup) 81 | expect(configuration.endpoint_liveness).to eq(endpoint_liveness) 82 | expect(configuration.endpoint_readiness).to eq(endpoint_readiness) 83 | expect(configuration.endpoint_startup_status_success).to eq(endpoint_startup_status_success) 84 | expect(configuration.endpoint_liveness_status_success).to eq(endpoint_liveness_status_success) 85 | expect(configuration.endpoint_readiness_status_success).to eq(endpoint_readiness_status_success) 86 | expect(configuration.endpoint_startup_status_failure).to eq(endpoint_startup_status_failure) 87 | expect(configuration.endpoint_liveness_status_failure).to eq(endpoint_liveness_status_failure) 88 | expect(configuration.endpoint_readiness_status_failure).to eq(endpoint_readiness_status_failure) 89 | end 90 | end 91 | end 92 | 93 | describe '.reset_configuration!' do 94 | before do 95 | described_class.configure( 96 | &configuration_block( 97 | services: services, 98 | services_startup: services_startup, 99 | services_liveness: services_liveness, 100 | services_readiness: services_readiness, 101 | endpoints_namespace: endpoints_namespace, 102 | endpoint_startup: endpoint_startup, 103 | endpoint_liveness: endpoint_liveness, 104 | endpoint_readiness: endpoint_readiness, 105 | endpoint_startup_status_success: endpoint_startup_status_success, 106 | endpoint_liveness_status_success: endpoint_liveness_status_success, 107 | endpoint_readiness_status_success: endpoint_readiness_status_success, 108 | endpoint_startup_status_failure: endpoint_startup_status_failure, 109 | endpoint_liveness_status_failure: endpoint_liveness_status_failure, 110 | endpoint_readiness_status_failure: endpoint_readiness_status_failure 111 | ) 112 | ) 113 | end 114 | 115 | it do 116 | expect { described_class.reset_configuration! } 117 | .to change(described_class, :configuration) 118 | .from(be_instance_of(HealthcheckEndpoint::Configuration)).to(nil) 119 | end 120 | end 121 | 122 | describe '.configuration' do 123 | subject(:configuration) { described_class.configuration } 124 | 125 | before do 126 | described_class.configure(&configuration_block( 127 | services: services, 128 | services_startup: services_startup, 129 | services_liveness: services_liveness, 130 | services_readiness: services_readiness, 131 | endpoints_namespace: endpoints_namespace, 132 | endpoint_startup: endpoint_startup, 133 | endpoint_liveness: endpoint_liveness, 134 | endpoint_readiness: endpoint_readiness, 135 | endpoint_startup_status_success: endpoint_startup_status_success, 136 | endpoint_liveness_status_success: endpoint_liveness_status_success, 137 | endpoint_readiness_status_success: endpoint_readiness_status_success, 138 | endpoint_startup_status_failure: endpoint_startup_status_failure, 139 | endpoint_liveness_status_failure: endpoint_liveness_status_failure, 140 | endpoint_readiness_status_failure: endpoint_readiness_status_failure 141 | )) 142 | end 143 | 144 | it 'returns configuration instance' do 145 | expect(configuration).to be_instance_of(HealthcheckEndpoint::Configuration) 146 | expect(configuration.services).to eq(services) 147 | expect(configuration.services_startup).to eq(services_startup) 148 | expect(configuration.services_liveness).to eq(services_liveness) 149 | expect(configuration.services_readiness).to eq(services_readiness) 150 | expect(configuration.endpoints_namespace).to eq(endpoints_namespace) 151 | expect(configuration.endpoint_startup).to eq(endpoint_startup) 152 | expect(configuration.endpoint_liveness).to eq(endpoint_liveness) 153 | expect(configuration.endpoint_readiness).to eq(endpoint_readiness) 154 | expect(configuration.endpoint_startup_status_success).to eq(endpoint_startup_status_success) 155 | expect(configuration.endpoint_liveness_status_success).to eq(endpoint_liveness_status_success) 156 | expect(configuration.endpoint_readiness_status_success).to eq(endpoint_readiness_status_success) 157 | expect(configuration.endpoint_startup_status_failure).to eq(endpoint_startup_status_failure) 158 | expect(configuration.endpoint_liveness_status_failure).to eq(endpoint_liveness_status_failure) 159 | expect(configuration.endpoint_readiness_status_failure).to eq(endpoint_readiness_status_failure) 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | rspec_custom = ::File.join(::File.dirname(__FILE__), 'support/**/*.rb') 4 | ::Dir[::File.expand_path(rspec_custom)].sort.each { |file| require file unless file[/\A.+_spec\.rb\z/] } 5 | 6 | require_relative '../lib/healthcheck_endpoint' 7 | 8 | RSpec.configure do |config| 9 | config.expect_with(:rspec) do |expectations| 10 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 11 | expectations.syntax = :expect 12 | end 13 | 14 | config.mock_with(:rspec) do |mocks| 15 | mocks.verify_partial_doubles = true 16 | end 17 | 18 | config.example_status_persistence_file_path = '.rspec_status' 19 | config.disable_monkey_patching! 20 | config.order = :random 21 | config.before { reset_configuration } 22 | 23 | config.include HealthcheckEndpoint::RspecHelper::ContextGenerator 24 | config.include HealthcheckEndpoint::RspecHelper::Configuration 25 | 26 | ::Kernel.srand(config.seed) 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/config/bundler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | -------------------------------------------------------------------------------- /spec/support/config/ffaker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ffaker' 4 | -------------------------------------------------------------------------------- /spec/support/config/json_matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'json_matchers/rspec' 4 | 5 | JsonMatchers.schema_root = 'spec/support/schemas' 6 | -------------------------------------------------------------------------------- /spec/support/config/pry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pry' if ::RUBY_VERSION[/\A3\.3.+\z/] 4 | -------------------------------------------------------------------------------- /spec/support/config/simplecov.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ::RUBY_VERSION[/\A3\.3.+\z/] 4 | require 'simplecov' 5 | 6 | SimpleCov.minimum_coverage(100) 7 | SimpleCov.start { add_filter(%r{\A/spec/}) } 8 | end 9 | -------------------------------------------------------------------------------- /spec/support/helpers/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module RspecHelper 5 | module Configuration 6 | def configuration_block(**configuration_settings) 7 | lambda do |config| 8 | configuration_settings.each do |attribute, value| 9 | config.public_send(:"#{attribute}=", value) 10 | end 11 | end 12 | end 13 | 14 | def create_configuration(**configuration_settings) 15 | HealthcheckEndpoint::Configuration.new(&configuration_block(**configuration_settings)) 16 | end 17 | 18 | def init_configuration(**args) 19 | HealthcheckEndpoint.configure( 20 | &configuration_block( 21 | **args 22 | ) 23 | ) 24 | end 25 | 26 | def current_configuration 27 | HealthcheckEndpoint.configuration 28 | end 29 | 30 | def reset_configuration 31 | HealthcheckEndpoint.reset_configuration! 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/support/helpers/context_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module HealthcheckEndpoint 4 | module RspecHelper 5 | module ContextGenerator 6 | def random_message 7 | ::FFaker::Lorem.sentence 8 | end 9 | 10 | def random_service_name 11 | ::FFaker::InternetSE.login_user_name 12 | end 13 | 14 | def random_endpoint 15 | "/#{random_service_name}" 16 | end 17 | 18 | def random_http_status(successful:) 19 | (successful ? 200..226 : 500..511).to_a.sample 20 | end 21 | 22 | def create_service(service_name, successful: true) 23 | { service_name => -> { successful } } 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/schemas/jsonapi_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2019-09/schema", 3 | "$id": "http://example.com/example.json", 4 | "title": "Root Schema", 5 | "type": "object", 6 | "default": {}, 7 | "required": [ 8 | "data" 9 | ], 10 | "properties": { 11 | "data": { 12 | "title": "The data Schema", 13 | "type": "object", 14 | "default": {}, 15 | "required": [ 16 | "id", 17 | "type", 18 | "attributes" 19 | ], 20 | "properties": { 21 | "id": { 22 | "title": "The id Schema", 23 | "type": "string", 24 | "default": "", 25 | "examples": [ 26 | "ea266931-feb6-4b19-9308-ec29f18f1c02" 27 | ] 28 | }, 29 | "type": { 30 | "title": "The type Schema", 31 | "type": "string", 32 | "default": "", 33 | "examples": [ 34 | "application-healthcheck" 35 | ] 36 | }, 37 | "attributes": { 38 | "title": "The attributes Schema", 39 | "type": "object", 40 | "default": {}, 41 | "properties": { 42 | "yettacoletta": { 43 | "title": "The yettacoletta Schema", 44 | "type": "boolean", 45 | "default": false, 46 | "examples": [ 47 | true 48 | ] 49 | }, 50 | "rubenwikstrm": { 51 | "title": "The rubenwikstrm Schema", 52 | "type": "boolean", 53 | "default": false, 54 | "examples": [ 55 | false 56 | ] 57 | } 58 | }, 59 | "examples": [{ 60 | "yettacoletta": true, 61 | "rubenwikstrm": false 62 | }] 63 | } 64 | }, 65 | "examples": [{ 66 | "id": "ea266931-feb6-4b19-9308-ec29f18f1c02", 67 | "type": "application-healthcheck", 68 | "attributes": { 69 | "yettacoletta": true, 70 | "rubenwikstrm": false 71 | } 72 | }] 73 | } 74 | }, 75 | "examples": [{ 76 | "data": { 77 | "id": "ea266931-feb6-4b19-9308-ec29f18f1c02", 78 | "type": "application-healthcheck", 79 | "attributes": { 80 | "yettacoletta": true, 81 | "rubenwikstrm": false 82 | } 83 | } 84 | }] 85 | } 86 | --------------------------------------------------------------------------------