Packages

Behaviour and use macro for Raven display integrations

Current section

Files

Jump to
raven_observer_display_sdk lib code_name_raven display.ex
Raw

lib/code_name_raven/display.ex

defmodule CodeNameRaven.Display do
@moduledoc """
The display behaviour for a Raven integration.
Handles the dashboard panel: rendering, layout hints, data preparation before
the render cycle, and optional user interaction handling.
## Relationship to Monitor
`CodeNameRaven.Monitor` defines the collection side — `collect/2`, `healthy?/1`,
`metrics/1`, and the collection lifecycle. `CodeNameRaven.Display` defines the
display side — what the panel looks like, how large it is, and how it responds
to user interaction. The two can live in the same module or in separate modules:
**Single-module (common case):**
defmodule Integrations.Http do
use CodeNameRaven.Monitor
use CodeNameRaven.Display, size_hint: :small
@impl CodeNameRaven.Monitor
def collect(params, state), do: ...
@impl CodeNameRaven.Monitor
def healthy?(result), do: ...
@impl CodeNameRaven.Display
def render(assigns) do
~H"..."
end
end
**Two-module (collection/display separation):**
# Collection side — may run on any node, including headless collectors
defmodule Integrations.Database do
use CodeNameRaven.Monitor
@impl true
def collect(params, state), do: ...
@impl true
def healthy?(result), do: ...
end
# Display side — runs only on nodes with raven_web
defmodule Integrations.Database.Panel do
use CodeNameRaven.Display, size_hint: :large
@impl true
def render(assigns) do
~H"..."
end
end
`CodeNameRaven.Monitor` never provides a display default of any kind — no
`render/1`, no `category/0`, nothing. A module gets a working panel only by
adding `use CodeNameRaven.Display` itself, whether bundled alongside Monitor
in the same module or as a companion module. There is no implicit shim and
no backwards-compatibility path between the two behaviours.
## Callbacks
"""
@current_display_api_version 2
@min_supported_display_api_version 1
@type size_hint :: :small | :medium | :large | :full
@type category :: atom()
# ---------------------------------------------------------------------------
# Callbacks
# ---------------------------------------------------------------------------
@doc """
Returns the API version this display module was written against.
Defaults to `2`. Used by the host application to verify compatibility with
the currently running version of `raven_sdk_display`.
"""
@callback display_api_version() :: pos_integer()
@doc """
Renders the integration's dashboard panel.
Called by the LiveView dashboard. Receives the assigns map built by Raven
and enriched by `prepare_assigns/1`. Compose from
`CodeNameRaven.MonitorComponents` or write raw HEEx.
This callback has no useful default — it must be implemented.
"""
@callback render(assigns :: map()) :: Phoenix.LiveView.Rendered.t()
@doc """
Enriches the assigns map before `render/1` is called.
Called once per dashboard tick, outside the render cycle. Use this to
fetch data (DB queries, sample reads) that should not happen on every
render pass. The returned map is passed directly to `render/1`.
The default is the identity function.
"""
@callback prepare_assigns(assigns :: map()) :: map()
@doc """
Returns the human-readable name for this integration.
Shown in the monitor catalogue and as the default panel title. The default
derives a name from the module name (e.g. `Integrations.Http``"Http"`).
"""
@callback display_name() :: String.t()
@doc """
Returns the category this integration belongs to.
Used to group integrations in the catalogue UI. Use a built-in category atom
or define your own. The default is `:custom`.
"""
@callback category() :: category()
@doc """
Declares the preferred panel size for the dashboard layout engine.
The engine uses this as a hint, not a hard constraint.
* `:small` — compact status indicator; one or two values
* `:medium` — a handful of metrics; default
* `:large` — multiple metric groups or a chart
* `:full` — full dashboard width; complex or multi-section displays
"""
@callback size_hint() :: size_hint()
@doc """
Returns a structured layout specification for the panel.
Used by the layout engine for placement control beyond the coarse size hint.
Return `nil` (the default) to let the engine derive placement from
`size_hint/0` alone. Reserved for future layout engine extensions — the
shape of this map is not yet stable.
"""
@callback layout() :: map() | nil
@doc """
Handles a LiveView event routed from the dashboard to this panel.
Called when the dashboard LiveView receives a `phx-click` or `phx-change`
event scoped to this panel. Receives the event name, the event params, and
the current socket. Return `{:noreply, socket}` to update state and
re-render.
The default is a no-op that leaves the socket unchanged.
"""
@callback handle_ui_event(
event :: String.t(),
params :: map(),
socket :: Phoenix.LiveView.Socket.t()
) :: {:noreply, Phoenix.LiveView.Socket.t()}
@doc """
Builds the display's initial subscriber state.
Called once when a configured Display instance starts, before it begins
receiving metric/status messages from the DataBus. The returned state is
threaded through `display_on_metric/3`/`display_on_status/3` and is what those callbacks
accumulate into over the instance's lifetime.
The default returns an empty map.
"""
@callback display_init(params :: map()) :: {:ok, term()} | {:error, String.t()}
@doc """
Handles a metric event from a watched monitor.
Called only for monitors on this Display instance's watch-list — the
runtime filters everything else before this callback is reached. `metric`
is a `CodeNameRaven.DataBus.MonitorMetric` struct at runtime — not typed as
such here since `raven_sdk` does not (yet) publish `CodeNameRaven.DataBus`.
Return the updated subscriber state.
The default leaves state unchanged.
"""
@callback display_on_metric(metric :: term(), params :: map(), state :: term()) :: {:ok, term()}
@doc """
Handles a status event from a watched monitor.
Same watch-list filtering as `display_on_metric/3`. `status` is a
`CodeNameRaven.DataBus.MonitorStatus` struct at runtime, same caveat as
`display_on_metric/3`. Return the updated subscriber state.
The default leaves state unchanged.
"""
@callback display_on_status(status :: term(), params :: map(), state :: term()) :: {:ok, term()}
@doc """
Declares which Monitor module(s) this display knows how to render.
Used by the instance-configuration UI to filter which monitors can be
added to a watch-list for this display — not consulted for any automatic,
type-wide enablement.
The default is an empty list.
"""
@callback compatible_monitors() :: [module()]
@doc """
Returns true if the given module implements the Display behaviour.
Mirrors `CodeNameRaven.Monitor.monitor_module?/1` — checks for `render/1`,
the one truly-required Display callback, the same way Monitor checks for
`collect/2`/`healthy?/1`.
"""
@spec display_module?(module()) :: boolean()
def display_module?(module) do
Code.ensure_loaded?(module) and function_exported?(module, :render, 1)
end
@doc """
Returns true if the given module has actually opted into the Display.Server
subscriber runtime (`display_init/1`, `on_metric/3`, `on_status/3`) —
narrower than `display_module?/1`.
The two checks usually agree — `use CodeNameRaven.Display` provides both
`render/1` and `display_init/1` together. They can diverge for a module
that hand-writes `render/1` itself without the macro (implementing the
behaviour directly rather than `use`-ing it) — `display_module?/1` would
see it as renderable, but it never got `display_init/1`. Use this check
wherever a real `Display.Server` instance is about to be started for the
module (e.g. bundled-default auto-fill, which needs `display_init/1` to
exist or `Display.Server.init/1` crashes) — not the dashboard-card-rendering
check `display_module?/1` is for.
"""
@spec display_server_module?(module()) :: boolean()
def display_server_module?(module) do
Code.ensure_loaded?(module) and function_exported?(module, :display_init, 1)
end
@doc """
Returns the current Display API version of the SDK.
"""
@spec current_display_api_version() :: pos_integer()
def current_display_api_version, do: @current_display_api_version
@doc """
Returns the minimum Display API version supported by the SDK.
"""
@spec min_supported_display_api_version() :: pos_integer()
def min_supported_display_api_version, do: @min_supported_display_api_version
@doc """
Returns true if the display module's declared `display_api_version/0` falls within
the supported range of this SDK.
"""
@spec display_api_version_supported?(module()) :: boolean()
def display_api_version_supported?(module) do
version =
if function_exported?(module, :display_api_version, 0) do
module.display_api_version()
else
# Legacy displays predating version tracking
1
end
version in @min_supported_display_api_version..@current_display_api_version
end
# ---------------------------------------------------------------------------
# __using__ — sets up the behaviour and provides default implementations
# ---------------------------------------------------------------------------
@doc false
defmacro __using__(opts \\ []) do
size_hint = Keyword.get(opts, :size_hint, :medium)
category = Keyword.get(opts, :category, :custom)
quote do
@behaviour CodeNameRaven.Display
use Phoenix.Component
@impl CodeNameRaven.Display
def display_api_version, do: 2
@impl CodeNameRaven.Display
def render(assigns) do
CodeNameRaven.MonitorComponents.default_render(assigns)
end
@impl CodeNameRaven.Display
def prepare_assigns(assigns), do: assigns
@impl CodeNameRaven.Display
def display_name do
CodeNameRaven.Monitor.derive_display_name(__MODULE__)
end
@impl CodeNameRaven.Display
def category, do: unquote(category)
@impl CodeNameRaven.Display
def size_hint, do: unquote(size_hint)
@impl CodeNameRaven.Display
def layout, do: nil
@impl CodeNameRaven.Display
def handle_ui_event(_event, _params, socket), do: {:noreply, socket}
@impl CodeNameRaven.Display
def display_init(_params), do: {:ok, %{}}
@impl CodeNameRaven.Display
def display_on_metric(_metric, _params, state), do: {:ok, state}
@impl CodeNameRaven.Display
def display_on_status(_status, _params, state), do: {:ok, state}
@impl CodeNameRaven.Display
def compatible_monitors, do: []
defoverridable display_api_version: 0,
render: 1,
prepare_assigns: 1,
display_name: 0,
category: 0,
size_hint: 0,
layout: 0,
handle_ui_event: 3,
display_init: 1,
display_on_metric: 3,
display_on_status: 3,
compatible_monitors: 0
end
end
end