defmodule CodeNameRaven.Handler do @moduledoc """ The behaviour contract for a Raven alert handler. A handler watches the DataBus for monitor status events and decides when to fire. When it fires, the platform calls every channel in the handler's config. Handlers run as supervised GenServer processes — one process per configured handler instance — giving them persistent state across events. This is the last piece of the alerting stack: Monitor → DataBus → Handler → Channel Monitors publish health signals. Handlers react to those signals. Channels deliver the notification. ## Implementing a handler defmodule MyHandlers.OnDown do use CodeNameRaven.Handler @impl true def handler_init(_params), do: {:ok, %{}} @impl true def on_status(%{status: :down} = msg, _params, state) do notification = %Notification{ monitor_id: msg.monitor_id, status: :down, previous_status: nil, occurred_at: msg.checked_at } {:fire, notification, state} end def on_status(_msg, _params, state), do: {:ok, state} end `on_status/3` is the only required callback — everything else has a working default. ## Return values from on_status/3 * `{:ok, new_state}` — no alert; update state and wait for the next event. * `{:fire, notification, new_state}` — deliver `notification` to every channel in this handler's config, then update state. ## The status argument `on_status/3`'s first argument is loosely typed (`term()`) rather than a concrete struct — the real message module (and its version/upcast machinery) lives in Raven itself, not this SDK, matching `CodeNameRaven.Probe.on_metric/3`'s precedent for the identical problem. It has (at least) these fields: `monitor_id` (string), `status` (`:up | :degraded | :down | :unknown`), `checked_at` (`DateTime.t()`), `node` (the reporting node, or `nil`/`:nonode@nohost` for a local-only signal). Raven upcasts the message to its current version before this callback ever sees it, so implementations never need to think about version compatibility themselves — pattern-match on the fields you need with a plain map pattern (it matches the real struct too, since structs are maps) rather than a named struct. ## State `handler_state` is opaque to the platform. Use it to track whatever the handler needs between events: the last known status per monitor, cooldown timers, deduplication keys. The state is ephemeral — a handler restart resets it via `handler_init/1`. """ alias CodeNameRaven.Channel.Notification # --------------------------------------------------------------------------- # Callbacks # --------------------------------------------------------------------------- @doc """ Initialises the handler's state. Called once when the handler process starts. Returns `{:ok, initial_state}` on success or `{:error, reason}` if the handler cannot start. The default implementation returns `{:ok, %{}}`. """ @callback handler_init(params :: map()) :: {:ok, term()} | {:error, String.t()} @doc """ Reacts to a monitor status event. Called for every status event published to the DataBus — see "The status argument" above for its shape. Return `{:ok, new_state}` to update state without firing, or `{:fire, notification, new_state}` to trigger channel delivery. The default implementation ignores all events and returns `{:ok, state}`. """ @callback on_status( status :: term(), params :: map(), state :: term() ) :: {:ok, term()} | {:fire, Notification.t(), term()} @doc """ Called periodically to perform timed tasks, such as checking for stale monitors. """ @callback on_tick( params :: map(), state :: term() ) :: {:ok, term()} | {:fire, Notification.t() | [Notification.t()], term()} @optional_callbacks on_tick: 2 @doc """ Returns the human-readable name for this handler type. """ @callback display_name() :: String.t() @doc """ Returns the category this handler belongs to. """ @callback category() :: atom() @doc """ Returns a template map of params this handler expects. Used by the admin UI as a fallback to derive a configuration form when `params_schema/0` is empty. """ @callback params_template() :: map() @doc """ Returns the NimbleOptions-style schema for validating/rendering this handler's params — the type-aware form the admin UI renders (checkbox for booleans, dropdown for a fixed set of choices) instead of a generic text box. Defaults to `[]` (no schema, fully backward compatible with every handler predating this callback — the UI falls back to `params_template/0` in that case). """ @callback params_schema() :: keyword() # --------------------------------------------------------------------------- # __using__ # --------------------------------------------------------------------------- @doc false defmacro __using__(opts \\ []) do category = Keyword.get(opts, :category, :alert) quote do @behaviour CodeNameRaven.Handler alias CodeNameRaven.Channel.Notification @impl CodeNameRaven.Handler def display_name, do: CodeNameRaven.Handler.derive_display_name(__MODULE__) @impl CodeNameRaven.Handler def category, do: unquote(category) @impl CodeNameRaven.Handler def params_template, do: %{} @impl CodeNameRaven.Handler def params_schema, do: [] @impl CodeNameRaven.Handler def handler_init(_params), do: {:ok, %{}} @impl CodeNameRaven.Handler def on_status(_msg, _params, state), do: {:ok, state} defoverridable display_name: 0, category: 0, params_template: 0, params_schema: 0, handler_init: 1, on_status: 3 end end # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @doc """ Returns true if the given module implements the Handler behaviour. """ @spec handler_module?(module()) :: boolean() def handler_module?(module) do Code.ensure_loaded?(module) and function_exported?(module, :on_status, 3) end @doc false @spec derive_display_name(module()) :: String.t() def derive_display_name(module) do module |> Module.split() |> List.last() |> Macro.underscore() |> String.replace("_", " ") |> String.split() |> Enum.map_join(" ", &String.capitalize/1) end end