Packages

Behaviour and use macro for Raven monitor integrations

Current section

Files

Jump to
raven_observer_sdk lib code_name_raven handler.ex
Raw

lib/code_name_raven/handler.ex

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