Packages

Behaviour and use macro for Raven monitor integrations

Current section

Files

Jump to
raven_observer_sdk lib code_name_raven channel.ex
Raw

lib/code_name_raven/channel.ex

defmodule CodeNameRaven.Channel do
@moduledoc """
The behaviour contract for a Raven notification channel.
A channel is a stateless delivery mechanism — it receives a `Notification`
and delivers it to an external system (webhook endpoint, email, XMPP,
PagerDuty, etc.). Channels do not run as processes; they are called
synchronously by the Handler when an alert condition is met.
## Implementing a channel
defmodule MyChannels.Slack do
use CodeNameRaven.Channel, category: :messaging
@impl true
def params_template, do: %{webhook_url: "https://hooks.slack.com/..."}
@impl true
def params_schema do
[webhook_url: [type: :string, required: true, doc: "Slack incoming webhook URL"]]
end
@impl true
def deliver(%Notification{} = n, %{webhook_url: url}) do
text = "\#{n.monitor_name} is \#{n.status}"
case Req.post(url, json: %{text: text}) do
{:ok, %{status: s}} when s in 200..299 -> :ok
{:ok, %{status: s}} -> {:error, "HTTP \#{s}"}
{:error, r} -> {:error, inspect(r)}
end
end
end
`deliver/2` is the only required callback — everything else has a working
default.
## Categories
Categories group channels in the catalogue UI. Use a built-in category or
define your own atom:
:webhook | :email | :messaging | :incident | :general
"""
alias CodeNameRaven.Channel.Notification
# ---------------------------------------------------------------------------
# Callbacks
# ---------------------------------------------------------------------------
@doc """
Delivers a notification to the external system.
Receives the `Notification` struct and the channel's params map. Returns
`:ok` on success or `{:error, reason}` on failure.
This callback is **required** — there is no default.
"""
@callback deliver(notification :: Notification.t(), params :: map()) ::
:ok | {:error, String.t()}
@doc """
Returns the human-readable name for this channel type.
Shown in the channel catalogue. The default derives a name from the
module suffix (e.g. `Channels.WebhookRelay``"Webhook Relay"`).
"""
@callback display_name() :: String.t()
@doc """
Returns the category this channel belongs to.
Used to group channels in the catalogue UI. The default is `:general`.
"""
@callback category() :: atom()
@doc """
Returns a template map of params this channel expects.
Used by the admin UI as a fallback to derive a configuration form when
`params_schema/0` is empty. The default is `%{}` (no params required).
Override in any channel that needs configuration:
def params_template, do: %{url: "https://", secret: nil}
"""
@callback params_template() :: map()
@doc """
Returns the NimbleOptions-style schema for validating/rendering this
channel'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 channel predating this callback — the UI falls back to
`params_template/0` in that case).
"""
@callback params_schema() :: keyword()
# ---------------------------------------------------------------------------
# __using__ — sets up the behaviour and provides default implementations
# ---------------------------------------------------------------------------
@doc false
defmacro __using__(opts \\ []) do
category = Keyword.get(opts, :category, :general)
quote do
@behaviour CodeNameRaven.Channel
@impl CodeNameRaven.Channel
def display_name, do: CodeNameRaven.Channel.derive_display_name(__MODULE__)
@impl CodeNameRaven.Channel
def category, do: unquote(category)
@impl CodeNameRaven.Channel
def params_template, do: %{}
@impl CodeNameRaven.Channel
def params_schema, do: []
defoverridable display_name: 0, category: 0, params_template: 0, params_schema: 0
end
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@doc """
Returns true if the given module implements the Channel behaviour.
Checks for the presence of `deliver/2`, which is the sole required callback.
"""
@spec channel_module?(module()) :: boolean()
def channel_module?(module) do
Code.ensure_loaded?(module) and
function_exported?(module, :deliver, 2)
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