├── .credo.exs ├── .formatter.exs ├── .github └── workflows │ └── elixir.yml ├── .gitignore ├── .iex.exs ├── .tool-versions ├── LICENSE ├── README.md ├── lib ├── plug_checkup.ex └── plug_checkup │ ├── check.ex │ ├── check │ ├── formatter.ex │ └── runner.ex │ └── options.ex ├── mix.exs ├── mix.lock ├── priv └── schemas │ └── health_check_response.json └── test ├── integration_test.exs ├── plug_checkup ├── check │ ├── formatter_test.exs │ └── runner_test.exs ├── check_test.exs └── options_test.exs ├── plug_checkup_test.exs ├── support └── my_checks.ex └── test_helper.exs /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/"], 7 | excluded: [~r"/_build/", ~r"/deps/", ~r"/doc/"] 8 | }, 9 | strict: true, 10 | color: true, 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", ".iex.exs", "{lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | env: 4 | MIX_ENV: test 5 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 6 | 7 | on: push 8 | 9 | jobs: 10 | format: 11 | needs: [test] 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Elixir 18 | uses: actions/setup-elixir@v1.5.0 19 | with: 20 | elixir-version: 1.9.4 21 | otp-version: 22.1.4 22 | install-hex: false 23 | install-rebar: false 24 | 25 | - name: Run Formatter 26 | run: mix format 27 | 28 | - name: Commit formatted files 29 | run: | 30 | git config --global user.name 'Elixir Formatter' 31 | git config --global user.email 'elixir-formatter@users.noreply.github.com' 32 | git diff --quiet && git diff --staged --quiet || (git commit -am "Automated code format"; git push) 33 | 34 | test: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | - name: Setup elixir 41 | uses: actions/setup-elixir@v1.5.0 42 | with: 43 | elixir-version: 1.9.4 44 | otp-version: 22.1.4 45 | 46 | - name: Install Dependencies 47 | run: | 48 | mix deps.get 49 | 50 | - name: Check warnings 51 | run: | 52 | mix compile --warnings-as-errors 53 | 54 | - name: Run Credo 55 | run: | 56 | mix credo 57 | 58 | - name: Run Tests 59 | run: | 60 | mix coveralls.github 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /.iex.exs: -------------------------------------------------------------------------------- 1 | alias PlugCheckup.Check 2 | alias PlugCheckup.Options 3 | 4 | defmodule Health.RandomCheck do 5 | def call do 6 | probability = :rand.uniform() 7 | 8 | cond do 9 | probability < 0.5 -> :ok 10 | probability < 0.6 -> {:error, "Error"} 11 | probability < 0.7 -> raise(RuntimeError, message: "Exception") 12 | probability < 0.8 -> exit(:boom) 13 | probability < 0.9 -> throw(:ball) 14 | true -> :timer.sleep(2000) 15 | end 16 | end 17 | end 18 | 19 | defmodule MyRouter do 20 | use Plug.Router 21 | 22 | plug(:match) 23 | plug(:dispatch) 24 | 25 | self_checks = [ 26 | %Check{name: "random1", module: Health.RandomCheck, function: :call}, 27 | %Check{name: "random2", module: Health.RandomCheck, function: :call} 28 | ] 29 | 30 | forward( 31 | "/selfhealth", 32 | to: PlugCheckup, 33 | init_opts: Options.new(json_encoder: Jason, checks: self_checks, timeout: 1000) 34 | ) 35 | 36 | deps_checks = [ 37 | %Check{name: "random1", module: Health.RandomCheck, function: :call}, 38 | %Check{name: "random2", module: Health.RandomCheck, function: :call} 39 | ] 40 | 41 | forward( 42 | "/dependencyhealth", 43 | to: PlugCheckup, 44 | init_opts: 45 | Options.new( 46 | json_encoder: Jason, 47 | checks: deps_checks, 48 | timeout: 3000, 49 | pretty: false, 50 | time_unit: :millisecond 51 | ) 52 | ) 53 | 54 | match _ do 55 | send_resp(conn, 404, "oops") 56 | end 57 | end 58 | 59 | {:ok, _} = Plug.Adapters.Cowboy.http(MyRouter, []) 60 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.9.4 2 | erlang 22.1.4 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Guilherme Gonçalves Pasqualino 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/ggpasqualino/plug_checkup/workflows/Build/badge.svg?branch=master)](https://github.com/ggpasqualino/plug_checkup/actions?query=branch%3Amaster) 2 | [![Coverage Status](https://coveralls.io/repos/github/ggpasqualino/plug_checkup/badge.svg?branch=master)](https://coveralls.io/github/ggpasqualino/plug_checkup?branch=master) 3 | || 4 | [![Hex version](https://img.shields.io/hexpm/v/plug_checkup.svg)](https://hex.pm/packages/plug_checkup) 5 | [![Hex downloads](https://img.shields.io/hexpm/dt/plug_checkup.svg)](https://hex.pm/packages/plug_checkup) 6 | 7 | # PlugCheckup 8 | 9 | PlugCheckup provides a Plug for adding simple health checks to your app. The [JSON output](#response) is similar to the one provided by the [MiniCheck](https://github.com/workshare/mini-check) Ruby library. It was started to provide [solarisBank](https://www.solarisbank.de/en/) an easy way of monitoring Plug based applications. 10 | 11 | ## Usage 12 | 13 | - Add the package to "mix.exs" 14 | ```elixir 15 | defp deps do 16 | [ 17 | {:plug_checkup, "~> 0.3.0"} 18 | ] 19 | end 20 | ``` 21 | 22 | - Create your [checks](#the-checks) 23 | ```elixir 24 | defmodule MyHealthChecks do 25 | def check_db do 26 | :ok 27 | end 28 | 29 | def check_redis do 30 | :ok 31 | end 32 | end 33 | ``` 34 | 35 | - Forward your health path to PlugCheckup in your Plug Router 36 | ```elixir 37 | checks = [ 38 | %PlugCheckup.Check{name: "DB", module: MyHealthChecks, function: :check_db}, 39 | %PlugCheckup.Check{name: "Redis", module: MyHealthChecks, function: :check_redis} 40 | ] 41 | 42 | forward( 43 | "/health", 44 | to: PlugCheckup, 45 | init_opts: PlugCheckup.Options.new(json_encoder: Jason, checks: checks) 46 | ) 47 | ``` 48 | 49 | If you're working with Phoenix, you need to change the syntax slightly to 50 | accomodate `Phoenix.Router.forward/4`: 51 | 52 | ```elixir 53 | checks = [ 54 | %PlugCheckup.Check{name: "DB", module: MyHealthChecks, function: :check_db}, 55 | %PlugCheckup.Check{name: "Redis", module: MyHealthChecks, function: :check_redis} 56 | ] 57 | 58 | forward("/health", PlugCheckup, PlugCheckup.Options.new(json_encoder: Jason, checks: checks)) 59 | ``` 60 | 61 | ## The Checks 62 | A check is a function with arity zero, which should return either :ok or {:error, term}. In case the check raises an exception or times out, that will be mapped to an {:error, reason} result. 63 | 64 | ## Response 65 | 66 | PlugCheckup should return either 200 or 500 statuses, Content-Type header "application/json", and the body should respect [this](priv/schemas/health_check_response.json) JSON schema 67 | 68 | If you configure the `error_code` option when initializing the plug, the specified value will be used when an error occurs instead of the 500 status. 69 | 70 | ## Demo 71 | 72 | Check [.iex.exs](.iex.exs) for a demo of plug_checkup in a Plug.Router. The demo can be run as following. 73 | ```sh 74 | git clone https://github.com/ggpasqualino/plug_checkup 75 | cd plug_checkup 76 | mix deps.get 77 | iex -S mix 78 | ``` 79 | Open your browse either on http://localhost:4000/selfhealth or http://localhost:4000/dependencyhealth 80 | -------------------------------------------------------------------------------- /lib/plug_checkup.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup do 2 | @moduledoc """ 3 | PlugCheckup provides a Plug for adding simple health checks to your app. 4 | """ 5 | 6 | alias PlugCheckup.Check.Formatter 7 | alias PlugCheckup.Check.Runner 8 | alias PlugCheckup.Options 9 | 10 | import Plug.Conn 11 | 12 | def init(options = %Options{}) do 13 | options 14 | end 15 | 16 | def call(conn, opts) do 17 | results = Runner.async_run(opts.checks, opts.timeout) 18 | 19 | conn 20 | |> put_resp_content_type("application/json") 21 | |> send_response(results, opts) 22 | end 23 | 24 | defp send_response(conn, {success, results}, opts) do 25 | status = 26 | case success do 27 | :ok -> 200 28 | :error -> opts.error_code 29 | end 30 | 31 | response = 32 | results 33 | |> Formatter.format(time_unit: opts.time_unit) 34 | |> opts.json_encoder.encode!(pretty: opts.pretty) 35 | 36 | send_resp(conn, status, response) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/plug_checkup/check.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.Check do 2 | @moduledoc """ 3 | Defines the structure of a check. 4 | Executes a check updating its result and execution time. Also, it transforms any exception into a tuple {:error, reason} 5 | """ 6 | 7 | @type t :: %PlugCheckup.Check{ 8 | name: String.t(), 9 | module: module(), 10 | function: atom(), 11 | result: atom(), 12 | time: pos_integer() 13 | } 14 | 15 | defstruct [:name, :module, :function, :result, :time] 16 | 17 | @spec execute(PlugCheckup.Check.t()) :: PlugCheckup.Check.t() 18 | def execute(check) do 19 | {time, result} = :timer.tc(&safe_execute/1, [check]) 20 | %{check | result: result, time: time} 21 | end 22 | 23 | @spec safe_execute(PlugCheckup.Check.t()) :: :ok | {:error, any} 24 | def safe_execute(check) do 25 | apply(check.module, check.function, []) 26 | rescue 27 | e -> {:error, Exception.message(e)} 28 | catch 29 | what, value -> {:error, "Caught #{what} with value: #{inspect(value)}"} 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/plug_checkup/check/formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.Check.Formatter do 2 | @external_resource "priv/schemas/health_check_response.json" 3 | @schema File.read!("priv/schemas/health_check_response.json") 4 | @moduledoc """ 5 | Format the given checks into a list of maps which respects the following JSON schema 6 | ```json 7 | #{@schema} 8 | ``` 9 | """ 10 | 11 | @spec format([PlugCheckup.Check.t()], keyword()) :: [map()] 12 | def format(checks, opts \\ []) 13 | 14 | def format(checks, opts) when is_list(checks) do 15 | Enum.map(checks, &format(&1, opts)) 16 | end 17 | 18 | @spec format(PlugCheckup.Check.t(), keyword()) :: map() 19 | def format(check, opts) do 20 | %{ 21 | "name" => check.name, 22 | "time" => time(check, opts), 23 | "healthy" => check.result == :ok, 24 | "error" => 25 | case check.result do 26 | :ok -> nil 27 | {:error, reason} -> reason 28 | end 29 | } 30 | end 31 | 32 | defp time(check, opts) do 33 | time_unit = opts[:time_unit] 34 | 35 | if time_unit in ~w(nil microsecond)a do 36 | check.time 37 | else 38 | System.convert_time_unit(check.time, :microsecond, time_unit) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/plug_checkup/check/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.Check.Runner do 2 | @moduledoc """ 3 | Executes the given checks asynchronously respecting the given timeout for each of the checks, 4 | and decides whether the execution was successful or not. 5 | """ 6 | alias PlugCheckup.Check 7 | 8 | @spec async_run([PlugCheckup.Check.t()], pos_integer()) :: tuple() 9 | def async_run(checks, timeout) do 10 | results = 11 | checks 12 | |> execute_all(timeout) 13 | |> Enum.zip(checks) 14 | |> Enum.map(&task_to_result/1) 15 | 16 | if Enum.all?(results, fn r -> r.result == :ok end) do 17 | {:ok, results} 18 | else 19 | {:error, results} 20 | end 21 | end 22 | 23 | @spec execute_all([PlugCheckup.Check.t()], pos_integer()) :: Enum.t() 24 | def execute_all(checks, timeout) do 25 | async_options = [timeout: timeout, on_timeout: :kill_task] 26 | Task.async_stream(checks, &Check.execute/1, async_options) 27 | end 28 | 29 | @spec task_to_result({ 30 | {:ok, PlugCheckup.Check.t()}, 31 | any 32 | }) :: PlugCheckup.Check.t() 33 | def task_to_result({{:ok, result}, _check}) do 34 | result 35 | end 36 | 37 | @spec task_to_result({ 38 | {:exit, any}, 39 | PlugCheckup.Check.t() 40 | }) :: PlugCheckup.Check.t() 41 | def task_to_result({{:exit, reason}, check}) do 42 | %{check | result: {:error, reason}} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/plug_checkup/options.ex: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.Options do 2 | @moduledoc """ 3 | Defines the options which can be given to initialize PlugCheckup. 4 | """ 5 | defstruct json_encoder: nil, 6 | timeout: :timer.seconds(1), 7 | checks: [], 8 | error_code: 500, 9 | pretty: true, 10 | time_unit: :microsecond 11 | 12 | @type t :: %__MODULE__{ 13 | json_encoder: module(), 14 | timeout: pos_integer(), 15 | checks: list(PlugCheckup.Check.t()), 16 | error_code: pos_integer(), 17 | pretty: boolean(), 18 | time_unit: :second | :millisecond | :microsecond 19 | } 20 | 21 | @spec new(keyword()) :: __MODULE__.t() 22 | def new(opts \\ []) do 23 | default = %__MODULE__{} 24 | Map.merge(default, fields_to_change(opts)) 25 | end 26 | 27 | defp fields_to_change(opts) do 28 | opts 29 | |> Keyword.take([:json_encoder, :timeout, :checks, :error_code, :pretty, :time_unit]) 30 | |> Enum.into(Map.new()) 31 | |> validate!() 32 | end 33 | 34 | defp validate!(fields) do 35 | validate_optional(fields, :timeout, &validate_timeout!/1) 36 | validate_optional(fields, :checks, &validate_checks!/1) 37 | validate_optional(fields, :error_code, &validate_error_code!/1) 38 | validate_optional(fields, :pretty, &validate_pretty!/1) 39 | validate_optional(fields, :time_unit, &validate_time_unit!/1) 40 | validate_required(fields, :json_encoder, &validate_json_encoder!/1) 41 | 42 | fields 43 | end 44 | 45 | defp validate_optional(fields, key, validator) do 46 | if Map.has_key?(fields, key) do 47 | value = fields[key] 48 | validator.(value) 49 | end 50 | end 51 | 52 | defp validate_required(fields, key, validator) do 53 | if Map.has_key?(fields, key) do 54 | value = fields[key] 55 | validator.(value) 56 | else 57 | raise ArgumentError, message: "PlugCheckup expects a #{inspect(key)} configuration" 58 | end 59 | end 60 | 61 | defp validate_error_code!(error_code) do 62 | if !is_integer(error_code) || error_code <= 0 do 63 | raise ArgumentError, message: "error_code should be a positive integer" 64 | end 65 | end 66 | 67 | defp validate_timeout!(timeout) do 68 | if !is_integer(timeout) || timeout <= 0 do 69 | raise ArgumentError, message: "timeout should be a positive integer" 70 | end 71 | end 72 | 73 | defp validate_checks!(checks) do 74 | not_a_check? = &(!match?(%{__struct__: PlugCheckup.Check}, &1)) 75 | 76 | if Enum.any?(checks, not_a_check?) do 77 | raise ArgumentError, message: "checks should be a list of PlugCheckup.Check" 78 | end 79 | end 80 | 81 | defp validate_pretty!(pretty) do 82 | if pretty not in [true, false] do 83 | raise ArgumentError, message: "pretty should be a boolean" 84 | end 85 | end 86 | 87 | defp validate_time_unit!(time_unit) do 88 | possible_values = ~w(second millisecond microsecond)a 89 | 90 | if time_unit not in possible_values do 91 | raise ArgumentError, message: "time_unit should be one of #{inspect(possible_values)}" 92 | end 93 | end 94 | 95 | defp validate_json_encoder!(encoder) do 96 | unless Code.ensure_compiled?(encoder) do 97 | raise ArgumentError, 98 | "invalid :json_encoder option. The module #{inspect(encoder)} is not " <> 99 | "loaded and could not be found" 100 | end 101 | 102 | unless function_exported?(encoder, :encode!, 2) do 103 | raise ArgumentError, 104 | "invalid :json_encoder option. The module #{inspect(encoder)} must " <> 105 | "implement encode!/2" 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :plug_checkup, 7 | version: "0.6.0", 8 | elixir: "~> 1.5", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | description: description(), 12 | package: package(), 13 | deps: deps(), 14 | test_coverage: [tool: ExCoveralls], 15 | preferred_cli_env: [ 16 | coveralls: :test, 17 | "coveralls.detail": :test, 18 | "coveralls.post": :test, 19 | "coveralls.html": :test 20 | ], 21 | name: "PlugCheckup", 22 | source_url: "https://github.com/ggpasqualino/plug_checkup" 23 | ] 24 | end 25 | 26 | def application do 27 | [] 28 | end 29 | 30 | defp elixirc_paths(:test), do: elixirc_paths() ++ ["test/support"] 31 | defp elixirc_paths(_), do: elixirc_paths() 32 | defp elixirc_paths(), do: ["lib"] 33 | 34 | def description do 35 | "PlugCheckup provides a Plug for adding simple health checks to your app." 36 | end 37 | 38 | def package do 39 | [ 40 | files: ["lib", "priv", "mix.exs", "README.md", "LICENSE"], 41 | maintainers: ["Guilherme Pasqualino"], 42 | licenses: ["MIT"], 43 | links: %{"GitHub" => "https://github.com/ggpasqualino/plug_checkup"} 44 | ] 45 | end 46 | 47 | defp deps do 48 | [ 49 | {:plug, "~> 1.4"}, 50 | {:jason, "~> 1.1", only: [:dev, :test]}, 51 | {:excoveralls, "~> 0.12", only: [:dev, :test]}, 52 | {:credo, ">= 0.0.0", only: [:dev, :test]}, 53 | {:ex_doc, ">= 0.0.0", only: :dev}, 54 | {:plug_cowboy, "~> 2.1", only: :dev}, 55 | {:ex_json_schema, "~> 0.7", only: :test} 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "certifi": {:hex, :certifi, "2.5.3", "70bdd7e7188c804f3a30ee0e7c99655bc35d8ac41c23e12325f36ab449b70651", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "ed516acb3929b101208a9d700062d520f3953da3b6b918d866106ffa980e1c10"}, 4 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 5 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.0", "69fdb5cf92df6373e15675eb4018cf629f5d8e35e74841bb637d6596cb797bbc", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "42868c229d9a2900a1501c5d0355bfd46e24c862c322b0b4f5a6f14fe0216753"}, 6 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 7 | "credo": {:hex, :credo, "1.5.4", "9914180105b438e378e94a844ec3a5088ae5875626fc945b7c1462b41afc3198", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cf51af45eadc0a3f39ba13b56fdac415c91b34f7b7533a13dc13550277141bc4"}, 8 | "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, 9 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, 10 | "ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"}, 11 | "ex_json_schema": {:hex, :ex_json_schema, "0.7.4", "09eb5b0c8184e5702bc89625a9d0c05c7a0a845d382e9f6f406a0fc1c9a8cc3f", [:mix], [], "hexpm", "45c67fa840f0d719a2b5578126dc29bcdc1f92499c0f61bcb8a3bcb5935f9684"}, 12 | "excoveralls": {:hex, :excoveralls, "0.13.4", "7b0baee01fe150ef81153e6ffc0fc68214737f54570dc257b3ca4da8e419b812", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "faae00b3eee35cdf0342c10b669a7c91f942728217d2a7c7f644b24d391e6190"}, 13 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 15 | "hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"}, 16 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 17 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 18 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 19 | "makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"}, 20 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, 21 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 22 | "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"}, 23 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 24 | "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, 25 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 26 | "plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"}, 27 | "plug_cowboy": {:hex, :plug_cowboy, "2.4.1", "779ba386c0915027f22e14a48919a9545714f849505fa15af2631a0d298abf0f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d72113b6dff7b37a7d9b2a5b68892808e3a9a752f2bf7e503240945385b70507"}, 28 | "plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"}, 29 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 30 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 31 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 32 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 33 | } 34 | -------------------------------------------------------------------------------- /priv/schemas/health_check_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Health check response", 4 | "type": "array", 5 | "items": { 6 | "title": "Check", 7 | "type": "object", 8 | "properties": { 9 | "name": { 10 | "description": "The name of this check, for example: 'redis', or 'postgres'", 11 | "type": "string" 12 | }, 13 | "healthy": { 14 | "description": "If the check was successful or not", 15 | "type" : "boolean" 16 | }, 17 | "time": { 18 | "description": "How long the check took to run in micros", 19 | "type": ["integer", "null"] 20 | }, 21 | "error": { 22 | "description": "The error reason in case the check fails", 23 | "type": ["string", "null"] 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IntegrationTest do 2 | use ExUnit.Case, async: true 3 | use Plug.Test 4 | 5 | defmodule Health.RandomCheck do 6 | def call do 7 | probability = :rand.uniform() 8 | 9 | cond do 10 | probability < 0.5 -> :ok 11 | probability < 0.7 -> {:error, "Error"} 12 | probability < 0.9 -> raise(RuntimeError, message: "Exception") 13 | true -> :timer.sleep(2000) 14 | end 15 | end 16 | end 17 | 18 | defmodule MyRouter do 19 | use Plug.Router 20 | 21 | alias Health.RandomCheck 22 | alias PlugCheckup, as: PC 23 | 24 | plug(:match) 25 | plug(:dispatch) 26 | 27 | checks = [ 28 | %PC.Check{name: "random1", module: RandomCheck, function: :call} 29 | ] 30 | 31 | forward( 32 | "/health", 33 | to: PC, 34 | init_opts: PC.Options.new(json_encoder: Jason, checks: checks, timeout: 1000) 35 | ) 36 | 37 | match _ do 38 | send_resp(conn, 404, "oops") 39 | end 40 | end 41 | 42 | @number_of_requests 100 43 | for i <- 1..@number_of_requests do 44 | test "response conforms to json schema - request number #{i}" do 45 | response = :get |> conn("/health") |> request() 46 | 47 | assert response.state == :sent 48 | assert response.status in [200, 500] 49 | assert_body_is_valid(response) 50 | end 51 | end 52 | 53 | def request(conn) do 54 | MyRouter.call(conn, MyRouter.init([])) 55 | end 56 | 57 | @external_resource "priv/schemas/health_check_response.json" 58 | @schema "priv/schemas/health_check_response.json" |> File.read!() |> Jason.decode!() 59 | def assert_body_is_valid(conn) do 60 | json_response = Jason.decode!(conn.resp_body) 61 | assert ExJsonSchema.Validator.validate(@schema, json_response) == :ok 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/plug_checkup/check/formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.Check.FormatterTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias PlugCheckup.Check 5 | alias PlugCheckup.Check.Formatter 6 | 7 | describe "format/2" do 8 | test "it formats successful check" do 9 | check = %Check{ 10 | name: "check 1", 11 | module: Fake, 12 | function: :call, 13 | result: :ok, 14 | time: 1234 15 | } 16 | 17 | formatted = Formatter.format(check) 18 | 19 | assert formatted == 20 | %{ 21 | "name" => "check 1", 22 | "time" => 1234, 23 | "healthy" => true, 24 | "error" => nil 25 | } 26 | end 27 | 28 | test "it formats unsuccessful check" do 29 | check = %Check{ 30 | name: "check 1", 31 | module: Fake, 32 | function: :call, 33 | result: {:error, "Error"}, 34 | time: 1234 35 | } 36 | 37 | formatted = Formatter.format(check) 38 | 39 | assert formatted == 40 | %{ 41 | "name" => "check 1", 42 | "time" => 1234, 43 | "healthy" => false, 44 | "error" => "Error" 45 | } 46 | end 47 | 48 | test "it formats a list of checks" do 49 | checks = [ 50 | %Check{ 51 | name: "check 1", 52 | module: Fake, 53 | function: :call, 54 | result: :ok, 55 | time: 1234 56 | } 57 | ] 58 | 59 | formatted = Formatter.format(checks) 60 | 61 | assert formatted == 62 | [ 63 | %{ 64 | "name" => "check 1", 65 | "time" => 1234, 66 | "healthy" => true, 67 | "error" => nil 68 | } 69 | ] 70 | end 71 | 72 | test "it formats the time" do 73 | check = %Check{ 74 | name: "check 1", 75 | module: Fake, 76 | function: :call, 77 | result: :ok, 78 | time: 1234 79 | } 80 | 81 | formatted = Formatter.format(check, time_unit: :millisecond) 82 | 83 | assert formatted == 84 | %{ 85 | "name" => "check 1", 86 | "time" => 1, 87 | "healthy" => true, 88 | "error" => nil 89 | } 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/plug_checkup/check/runner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.Check.RunnerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias PlugCheckup.Check 5 | alias PlugCheckup.Check.Runner 6 | 7 | describe "task_to_result/1" do 8 | test "it keeps the result on success" do 9 | check = %Check{result: :ok} 10 | assert Runner.task_to_result({{:ok, check}, check}) == check 11 | end 12 | 13 | test "it updates result on timeout" do 14 | check = %Check{result: nil} 15 | expected = %{check | result: {:error, "Timeout"}} 16 | assert Runner.task_to_result({{:exit, "Timeout"}, check}) == expected 17 | end 18 | end 19 | 20 | describe "execute_all/2" do 21 | test "it executes all checks within the timeout" do 22 | checks = [ 23 | %Check{name: "1", module: MyChecks, function: :raise_exception}, 24 | %Check{name: "2", module: MyChecks, function: :execute_successfuly}, 25 | %Check{name: "3", module: MyChecks, function: :execute_with_error}, 26 | %Check{name: "4", module: MyChecks, function: :execute_long_time} 27 | ] 28 | 29 | assert [ 30 | ok: %Check{ 31 | name: "1", 32 | module: MyChecks, 33 | function: :raise_exception, 34 | result: {:error, "Exception"}, 35 | time: _ 36 | }, 37 | ok: %Check{ 38 | name: "2", 39 | module: MyChecks, 40 | function: :execute_successfuly, 41 | result: :ok, 42 | time: _ 43 | }, 44 | ok: %Check{ 45 | name: "3", 46 | module: MyChecks, 47 | function: :execute_with_error, 48 | result: {:error, "Error"}, 49 | time: _ 50 | }, 51 | exit: :timeout 52 | ] = checks |> Runner.execute_all(500) |> Enum.to_list() 53 | end 54 | end 55 | 56 | describe "async_run/2" do 57 | test "it executes all checks and returns ok when all checks are successful" do 58 | checks = [ 59 | %Check{name: "1", module: MyChecks, function: :execute_successfuly}, 60 | %Check{name: "2", module: MyChecks, function: :execute_successfuly} 61 | ] 62 | 63 | assert {:ok, 64 | [ 65 | %Check{ 66 | name: "1", 67 | module: MyChecks, 68 | function: :execute_successfuly, 69 | result: :ok, 70 | time: _ 71 | }, 72 | %Check{ 73 | name: "2", 74 | module: MyChecks, 75 | function: :execute_successfuly, 76 | result: :ok, 77 | time: _ 78 | } 79 | ]} = Runner.async_run(checks, 500) 80 | end 81 | 82 | test "it executes all checks and returns error when any check is unsuccessful" do 83 | checks = [ 84 | %Check{name: "1", module: MyChecks, function: :execute_with_error}, 85 | %Check{name: "2", module: MyChecks, function: :execute_successfuly} 86 | ] 87 | 88 | assert {:error, 89 | [ 90 | %Check{ 91 | name: "1", 92 | module: MyChecks, 93 | function: :execute_with_error, 94 | result: {:error, "Error"}, 95 | time: _ 96 | }, 97 | %Check{ 98 | name: "2", 99 | module: MyChecks, 100 | function: :execute_successfuly, 101 | result: :ok, 102 | time: _ 103 | } 104 | ]} = Runner.async_run(checks, 500) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/plug_checkup/check_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.CheckTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias PlugCheckup.Check 5 | 6 | describe "safe_execute/1" do 7 | test "it converts exceptions to errors" do 8 | check = %Check{module: MyChecks, function: :raise_exception} 9 | assert Check.safe_execute(check) == {:error, "Exception"} 10 | end 11 | 12 | test "it converts exceptions without message field to errors" do 13 | check = %Check{module: MyChecks, function: :non_existent_function} 14 | 15 | assert Check.safe_execute(check) == 16 | {:error, "function MyChecks.non_existent_function/0 is undefined or private"} 17 | end 18 | 19 | test "it converts exit values" do 20 | check = %Check{module: MyChecks, function: :exit} 21 | assert Check.safe_execute(check) == {:error, "Caught exit with value: :boom"} 22 | end 23 | 24 | test "it converts thrown values" do 25 | check = %Check{module: MyChecks, function: :throw} 26 | assert Check.safe_execute(check) == {:error, "Caught throw with value: :ball"} 27 | end 28 | 29 | test "it doesn't change error results" do 30 | check = %Check{module: MyChecks, function: :execute_with_error} 31 | assert Check.safe_execute(check) == {:error, "Error"} 32 | end 33 | 34 | test "it doesn't change succes results" do 35 | check = %Check{module: MyChecks, function: :execute_successfuly} 36 | assert Check.safe_execute(check) == :ok 37 | end 38 | end 39 | 40 | describe "execute/1" do 41 | test "executes check safely and update result" do 42 | check = %Check{module: MyChecks, function: :raise_exception} 43 | %Check{result: result} = Check.execute(check) 44 | assert {:error, "Exception"} == result 45 | end 46 | 47 | test "updates execution time" do 48 | check = %Check{module: MyChecks, function: :raise_exception} 49 | %Check{time: time} = Check.execute(check) 50 | assert is_integer(time) 51 | assert time > 0 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/plug_checkup/options_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckup.OptionsTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias PlugCheckup.Options 5 | 6 | describe "default values" do 7 | test "it has default timeout" do 8 | options = new_options() 9 | assert options.timeout == :timer.seconds(1) 10 | end 11 | 12 | test "it has default checks" do 13 | options = new_options() 14 | assert options.checks == [] 15 | end 16 | 17 | test "it has default error_code" do 18 | options = new_options() 19 | assert options.error_code == 500 20 | end 21 | 22 | test "it has default pretty" do 23 | options = new_options() 24 | assert options.pretty == true 25 | end 26 | 27 | test "it has default time_uit" do 28 | options = new_options() 29 | assert options.time_unit == :microsecond 30 | end 31 | end 32 | 33 | describe "setting timeout" do 34 | test "timeout is set for positive integers" do 35 | options = new_options(timeout: 12) 36 | assert options.timeout == 12 37 | end 38 | 39 | test "it raises exception when timeout is zero" do 40 | assert_raise(ArgumentError, "timeout should be a positive integer", fn -> 41 | new_options(timeout: 0) 42 | end) 43 | end 44 | 45 | test "it raises exception when timeout is negative" do 46 | assert_raise(ArgumentError, "timeout should be a positive integer", fn -> 47 | new_options(timeout: -1) 48 | end) 49 | end 50 | 51 | test "it raises exception when timeout is not integer" do 52 | assert_raise(ArgumentError, "timeout should be a positive integer", fn -> 53 | new_options(timeout: :atom) 54 | end) 55 | end 56 | end 57 | 58 | describe "setting checks" do 59 | test "checks is set for a list of PlugCheckup.Check" do 60 | check = %PlugCheckup.Check{name: "check 1"} 61 | options = new_options(checks: [check]) 62 | assert options.checks == [check] 63 | end 64 | 65 | test "it raises exception when checks is not a list of PlugCheckup.Check" do 66 | assert_raise(ArgumentError, "checks should be a list of PlugCheckup.Check", fn -> 67 | check = %{} 68 | new_options(checks: [check]) 69 | end) 70 | end 71 | end 72 | 73 | describe "setting error_code" do 74 | test "error_code is set for positive integers" do 75 | options = new_options(error_code: 12) 76 | assert options.error_code == 12 77 | end 78 | 79 | test "it raises exception when error_code is zero" do 80 | assert_raise(ArgumentError, "error_code should be a positive integer", fn -> 81 | new_options(error_code: 0) 82 | end) 83 | end 84 | 85 | test "it raises exception when error_code is negative" do 86 | assert_raise(ArgumentError, "error_code should be a positive integer", fn -> 87 | new_options(error_code: -1) 88 | end) 89 | end 90 | 91 | test "it raises exception when error_code is not integer" do 92 | assert_raise(ArgumentError, "error_code should be a positive integer", fn -> 93 | new_options(error_code: :atom) 94 | end) 95 | end 96 | end 97 | 98 | describe "setting pretty" do 99 | test "pretty is set for a boolean" do 100 | options = new_options(pretty: false) 101 | assert options.pretty == false 102 | end 103 | 104 | test "it raises exception when pretty is not a boolean" do 105 | assert_raise(ArgumentError, "pretty should be a boolean", fn -> 106 | new_options(pretty: :not_a_boolean) 107 | end) 108 | end 109 | end 110 | 111 | describe "setting time_unit" do 112 | test "time_unit is a valid value" do 113 | options = new_options(time_unit: :second) 114 | assert options.time_unit == :second 115 | end 116 | 117 | test "it raises exception when time_unit is not a valid value" do 118 | assert_raise( 119 | ArgumentError, 120 | "time_unit should be one of [:second, :millisecond, :microsecond]", 121 | fn -> 122 | new_options(time_unit: :foo) 123 | end 124 | ) 125 | end 126 | end 127 | 128 | describe "setting json_encoder" do 129 | test "json_encoder is required" do 130 | assert_raise(ArgumentError, "PlugCheckup expects a :json_encoder configuration", fn -> 131 | Options.new() 132 | end) 133 | end 134 | 135 | test "it raises exception when json_encoder module is not loaded" do 136 | assert_raise( 137 | ArgumentError, 138 | "invalid :json_encoder option. The module Poison is not loaded and could not be found", 139 | fn -> 140 | new_options(json_encoder: Poison) 141 | end 142 | ) 143 | end 144 | 145 | test "it raises exception when json_encoder module does't expose encode!/2 function" do 146 | assert_raise( 147 | ArgumentError, 148 | "invalid :json_encoder option. The module PlugCheckup must implement encode!/2", 149 | fn -> 150 | new_options(json_encoder: PlugCheckup) 151 | end 152 | ) 153 | end 154 | 155 | test "json_encoder is a valid value" do 156 | options = new_options(json_encoder: Jason) 157 | assert options.json_encoder == Jason 158 | end 159 | end 160 | 161 | defp new_options(opts \\ []) do 162 | [json_encoder: Jason] 163 | |> Keyword.merge(opts) 164 | |> Options.new() 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /test/plug_checkup_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugCheckupTest do 2 | use ExUnit.Case, async: true 3 | use Plug.Test 4 | 5 | alias PlugCheckup.Check 6 | alias PlugCheckup.Options 7 | 8 | def execute_plug(:healthy) do 9 | check = %Check{name: "1", module: MyChecks, function: :execute_successfuly} 10 | execute_plug(check) 11 | end 12 | 13 | def execute_plug(:not_healthy) do 14 | check = %Check{name: "1", module: MyChecks, function: :execute_with_error} 15 | execute_plug(check) 16 | end 17 | 18 | def execute_plug(check = %Check{}) do 19 | options = PlugCheckup.init(Options.new(json_encoder: Jason, checks: [check])) 20 | request = conn(:get, "/") 21 | 22 | PlugCheckup.call(request, options) 23 | end 24 | 25 | describe "status" do 26 | test "it is 200 when healthy" do 27 | response = execute_plug(:healthy) 28 | assert response.status == 200 29 | end 30 | 31 | test "it is 500 when not healthy" do 32 | response = execute_plug(:not_healthy) 33 | assert response.status == 500 34 | end 35 | end 36 | 37 | describe "content-type" do 38 | test "it is 'application/json' when healthy" do 39 | response = execute_plug(:healthy) 40 | assert get_resp_header(response, "content-type") == ["application/json; charset=utf-8"] 41 | end 42 | 43 | test "it is 'application/json' when not healthy" do 44 | response = execute_plug(:not_healthy) 45 | assert get_resp_header(response, "content-type") == ["application/json; charset=utf-8"] 46 | end 47 | end 48 | 49 | describe "body" do 50 | test "it is a valid json when healthy" do 51 | response = execute_plug(:healthy) 52 | formatted_response = Jason.decode!(response.resp_body) 53 | 54 | assert [ 55 | %{ 56 | "name" => "1", 57 | "time" => time, 58 | "healthy" => true, 59 | "error" => nil 60 | } 61 | ] = formatted_response 62 | 63 | assert is_integer(time) 64 | end 65 | 66 | test "it is a valid json when not healthy" do 67 | response = execute_plug(:not_healthy) 68 | formatted_response = Jason.decode!(response.resp_body) 69 | 70 | assert [ 71 | %{ 72 | "name" => "1", 73 | "time" => time, 74 | "healthy" => false, 75 | "error" => "Error" 76 | } 77 | ] = formatted_response 78 | 79 | assert is_integer(time) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/support/my_checks.ex: -------------------------------------------------------------------------------- 1 | defmodule MyChecks do 2 | @moduledoc """ 3 | Mock for checks 4 | """ 5 | 6 | def execute_successfuly do 7 | :ok 8 | end 9 | 10 | def execute_with_error do 11 | {:error, "Error"} 12 | end 13 | 14 | def exit do 15 | exit(:boom) 16 | end 17 | 18 | def throw do 19 | throw(:ball) 20 | end 21 | 22 | def raise_exception do 23 | raise RuntimeError, message: "Exception" 24 | end 25 | 26 | def execute_long_time do 27 | 1 |> :timer.seconds() |> :timer.sleep() 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------