Packages

A process to periodically send heartbeat to monitoring service

Current section

Files

Jump to
push_heartbeat_monitor lib push_heartbeat_monitor.ex
Raw

lib/push_heartbeat_monitor.ex

defmodule PushHeartbeatMonitor do
@moduledoc """
Documentation for `PushHeartbeatMonitor`.
"""
use GenServer
require Logger
@typedoc """
* `push_url` - url to push to monitor service, eg: https://gatus.acme.org/api/v1/endpoints/mygroup_myservice/external
* `push_success_arg` - success query param to set on the monitor service, eg: `success=true` or `status=up`
* `push_failure_arg` - failure query param to set on the monitor service, eg: `success=false` or `status=down`
* `push_token` - token to authenticate to the monitor service, eg: `11111111-1111-1111-1111-111111111111`
* `push_token_placement` - where to put token for authentication, eg: `:bearer` or `{:header, "x-api-key"}`
* `push_message_param` - message to set on the monitor service eg: `error` or `msg`
* `push_method` - method to send the push request, eg: `:get`
* `push_interval_in_seconds` - push interval in seconds, eg: `20`
* `healthcheck_function` - module function for custom health check logic, return `:ok` or `{:error, "message"}`, eg: `{MyHealthCheckModule, :check_all}`
"""
@type config :: %{
:push_url => String.t(),
:push_success_arg => String.t(),
:push_failure_arg => String.t(),
optional(:push_token) => String.t(),
optional(:push_token_placement) => :bearer | {:header, String.t()},
optional(:push_message_param) => String.t(),
optional(:push_method) => :get | :post,
optional(:push_interval_in_seconds) => non_neg_integer(),
optional(:healthcheck_function) => nil | {module(), atom()}
}
@default_config %{
push_message_param: "msg",
push_method: :post,
push_interval_in_seconds: 20,
healthcheck_function: nil
}
@spec start_link(opts :: config()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
@impl true
def init(opts) do
config = Map.merge(@default_config, opts)
config = Map.put(config, :push_interval, config.push_interval_in_seconds * 1000)
{:ok, %{config: config}, {:continue, :push_monitor}}
end
@impl true
def handle_continue(:push_monitor, state) do
Logger.info("push heartbeat monitor started")
:ok = Process.send(self(), :push_monitor, [])
{:noreply, state}
end
@impl true
def handle_info(:push_monitor, state) do
_ref = Process.send_after(self(), :push_monitor, state.config.push_interval)
case do_push_heartbeat(state.config) do
{:ok, %Req.Response{status: status}} when status in [200, 204] ->
Logger.debug("push heartbeat sent")
{:ok, %Req.Response{status: status, body: resp}} ->
Logger.warning("failed to push heartbeat - status:#{status}, resp:#{resp}")
{:error, exception} ->
Logger.error("error pushing heartbeat - #{Exception.message(exception)}")
end
{:noreply, state}
end
defp do_push_heartbeat(%{healthcheck_func: {mod, func}} = config) do
healthcheck = apply(mod, func, [])
push_url = construct_push_url_with_params(config, healthcheck)
apply(Req, config.push_method, [push_url, [headers: set_auth_header(config)]])
end
defp do_push_heartbeat(config) do
push_url = construct_push_url_with_params(config, :ok)
apply(Req, config.push_method, [push_url, [headers: set_auth_header(config)]])
end
defp construct_push_url_with_params(config, :ok),
do: "#{config.push_url}?#{config.push_success_arg}"
defp construct_push_url_with_params(config, {:error, message}),
do: "#{config.push_url}?#{config.push_failure_arg}&#{config.push_message_param}=#{message}"
defp set_auth_header(%{push_token: push_token, push_token_placement: :bearer}),
do: [{"authorization", "Bearer #{push_token}"}]
defp set_auth_header(%{push_token: push_token, push_token_placement: {:header, header}}),
do: [{header, push_token}]
defp set_auth_header(_config),
do: []
end