core/sup_tree_common/alert_manager.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

use Croma

defmodule AntikytheraCore.Alert.Manager do
  @moduledoc """
  An event manager hosting multiple `AntikytheraCore.Alert.Handler`s.

  Managers are spawned per OTP application (antikythera and each gear).
  Each handler has its own buffer to store messages before sending them as an alert.
  """

  alias Antikythera.{GearName, Time}
  require AntikytheraCore.Logger, as: L
  alias AntikytheraCore.GearModule
  alias AntikytheraCore.Alert.Handler, as: AHandler
  alias AntikytheraCore.Alert.{HandlerConfigsMap, ErrorCountReporter}

  @handler_module_prefix "AntikytheraCore.Alert.Handler."

  def child_spec(args) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [args]}
    }
  end

  def start_link([otp_app_name, name_to_register]) do
    {:ok, pid} = :gen_event.start_link({:local, name_to_register})
    update_handler_installations(otp_app_name, HandlerConfigsMap.get(otp_app_name))
    {:ok, pid}
  end

  @doc """
  Update handler installations according to alert config of an OTP application (antikythera or gear).
  Handlers will be installed/uninstalled depending on contents of the config.
  If the config has sufficient information for a handler, it will be installed (noop if already installed).
  Otherwise, it will not be installed and will be uninstalled if it is already installed.

  Any errors will be logged but this function always returns `:ok`.
  """
  defun update_handler_installations(
          otp_app_name :: v[:antikythera | GearName.t()],
          configs_map :: v[HandlerConfigsMap.t()]
        ) :: :ok do
    case manager(otp_app_name) do
      # log as info since this will always happen on initial loading of gear configs (before gear installations)
      nil -> L.info("Alert manager process for '#{otp_app_name}' is not running!")
      pid -> update_handler_installations_impl(pid, otp_app_name, configs_map)
    end
  end

  defp update_handler_installations_impl(manager, otp_app_name, configs_map) do
    installed_handlers = :gen_event.which_handlers(manager)
    handlers_to_install = handlers_to_install(configs_map)
    to_remove = installed_handlers -- handlers_to_install
    to_add = handlers_to_install -- installed_handlers

    remove_results =
      Enum.map(to_remove, fn handler ->
        :gen_event.delete_handler(manager, handler, :remove_handler)
      end)

    add_results =
      Enum.map(to_add, fn handler ->
        :gen_event.add_handler(manager, handler, handler_init_arg(otp_app_name, handler))
      end)

    case Enum.reject(remove_results ++ add_results, &(&1 == :ok)) do
      [] ->
        :ok

      errors ->
        reasons = Enum.map(errors, fn {:error, reason} -> reason end)

        L.error(
          "Failed to install some alert handler(s) for '#{otp_app_name}':\n#{inspect(reasons)}"
        )

        :ok
    end
  end

  defunp handlers_to_install(configs_map :: v[HandlerConfigsMap.t()]) :: [:gen_event.handler()] do
    handler_pairs =
      configs_map
      |> Enum.map(fn {k, conf} -> {key_to_handler(k), conf} end)
      |> Enum.filter(fn {h, conf} -> h.validate_config(conf) end)
      |> Enum.map(fn {h, _} -> {AHandler, h} end)

    [ErrorCountReporter | handler_pairs]
  end

  defp handler_init_arg(otp_app_name, ErrorCountReporter), do: otp_app_name
  defp handler_init_arg(otp_app_name, {AHandler, handler}), do: {otp_app_name, handler}

  defp key_to_handler(key) do
    temporary_module_name_str = @handler_module_prefix <> Macro.camelize(key)

    # handlers must be chosen from list of existing handlers (in console UI), so this should never raise
    Module.safe_concat([temporary_module_name_str])
  end

  defunp manager(otp_app_name :: v[:antikythera | GearName.t()]) :: nil | pid do
    try do
      case otp_app_name do
        :antikythera -> AntikytheraCore.Alert.Manager
        gear_name -> GearModule.alert_manager(gear_name)
      end
      |> Process.whereis()
    rescue
      ArgumentError -> nil
    end
  end

  #
  # API for handlers
  #
  @doc """
  Schedule next flushing from handlers. Assuming the `interval` is in seconds.
  Note that handlers are kept as state in gen_event manager process,
  thus calling `self/0` always refers to the manager process which the handler is installed in.
  """
  defun schedule_handler_timeout(handler :: v[module], interval :: v[pos_integer]) :: :ok do
    Process.send_after(self(), {:handler_timeout, handler}, interval * 1_000)
    :ok
  end

  #
  # Public API
  #
  @doc """
  Notify a `manager` process of a message `body` with optional `time`.
  """
  defun notify(
          manager :: v[atom],
          body :: v[String.t()],
          time :: v[Time.t()] \\ Time.now()
        ) :: :ok do
    :gen_event.notify(manager, {time, body})
  end
end