defmodule CodeNameRaven.Probe do @moduledoc """ Behaviour for probe monitors — monitors that observe other monitors via Raven's event bus rather than collecting from an external target directly. A probe subscribes to events on startup (handled by Raven itself, not the probe) and reacts to them as they arrive. Unlike a standard integration, a probe never polls anything — its "collection" is just reading its own accumulated state, updated as events arrive. This module is deliberately implementation-free: it defines only the callback contract. The actual event subscription and dispatch live in Raven itself (mirroring how `CodeNameRaven.Display`'s `on_metric/3`/ `on_status/3` work — the display module never subscribes to anything itself either). A probe module built against this behaviour has zero dependency on any Raven-internal module and compiles standalone. ## Using this behaviour Replace `use CodeNameRaven.Monitor` with `use CodeNameRaven.Probe`. Then implement the callbacks your probe needs: * `probe_init/1` — set up initial state from params (replaces `init/1`) * `on_metric/3` — react to an arriving metric event * `on_status/3` — react to an arriving status event (optional) * `healthy?/1` — evaluate current state → health signal (required, inherited from `CodeNameRaven.Monitor` — not redefined here) All callbacks except `healthy?/1` have no-op defaults. A minimal probe only needs `probe_init/1`, `on_metric/3`, and `healthy?/1`. ## Lifecycle 1. Raven starts the probe's process, sees it's probe-shaped, and subscribes it to metric and status events on its behalf. 2. Raven calls `init/1` → delegates to `probe_init/1` for state setup. 3. As metric/status events arrive, Raven calls `on_metric/3` or `on_status/3` directly with the event and the probe's current state. 4. `on_metric/3` / `on_status/3` return `{:ok, new_state}` to update state and wait, or `{:ok, new_state, :check_now}` to also trigger an immediate check cycle. 5. On each check cycle, `collect/2` returns `{:ok, state, state}` — the current state is the collect result. 6. `healthy?/1` receives the state and returns `:up`, `:degraded`, `:down`, or `:unknown`. ## Event shapes `on_metric/3`'s first argument and `on_status/3`'s first argument are loosely typed (`term()`) rather than a concrete struct — the concrete shape is documented here rather than enforced at compile time, matching `CodeNameRaven.Display`'s `on_metric/3`/`on_status/3` precedent, since the real message module lives in Raven itself, not this SDK. A metric event has (at least) these fields: `monitor_id` (the id of the monitor that produced it), `metric_name` (string), `value` (number), `checked_at` (`DateTime.t()`), `target_id` (string or `nil`). A status event has (at least): `monitor_id`, `status` (`:up | :degraded | :down | :unknown`), `checked_at`, `target_id`. Both are plain structs with a stable, additive-only field contract: Raven may add fields to these events across releases, but will not remove or repurpose a field a probe already relies on. ## Example defmodule Integrations.MyProbe do use CodeNameRaven.Probe use CodeNameRaven.Display, category: :custom, size_hint: :small @impl true def probe_init(%{"monitor_id" => mid} = _params) do {:ok, %{monitor_id: mid, last_value: nil}} end @impl true def on_metric(%{monitor_id: mid, metric_name: "my_metric", value: v}, _params, %{monitor_id: mid} = state) do {:ok, %{state | last_value: v}, :check_now} end def on_metric(_, _, state), do: {:ok, state} @impl true def healthy?(%{last_value: nil}), do: :unknown def healthy?(%{last_value: v}) when v > 100, do: :down def healthy?(_), do: :up end ## Relationship to Monitor `use CodeNameRaven.Probe` sets up the full `CodeNameRaven.Monitor` behaviour and overrides two callbacks with probe-specific defaults: * `init/1` → delegates to `probe_init/1` * `collect/2` → returns `{:ok, state, state}` (state is the result) All other Monitor callbacks (`after_check/3`, `metrics/1`, etc.) are available with their standard defaults and can be overridden normally. """ # --------------------------------------------------------------------------- # Callbacks # --------------------------------------------------------------------------- @doc """ Initialises the probe's state from params. Called by the Probe-managed `init/1`. Return `{:ok, initial_state}` or `{:error, reason}`. The default returns `{:ok, %{}}`. """ @callback probe_init(params :: map()) :: {:ok, term()} | {:error, String.t()} @doc """ Handles an arriving metric event. Called directly by Raven when a metric event matching this probe's subscription arrives — not routed through `handle_info/3`. Return `{:ok, new_state}` to update state, or `{:ok, new_state, :check_now}` to also trigger an immediate check cycle. The default ignores all metrics and returns the state unchanged. """ @callback on_metric(metric :: term(), params :: map(), state :: term()) :: {:ok, term()} | {:ok, term(), :check_now} @doc """ Handles an arriving status event. Called directly by Raven, same delivery mechanism as `on_metric/3`. Return `{:ok, new_state}` to update state, or `{:ok, new_state, :check_now}` to also trigger an immediate check cycle. The default ignores all status events and returns the state unchanged. """ @callback on_status(status :: term(), params :: map(), state :: term()) :: {:ok, term()} | {:ok, term(), :check_now} @doc """ Returns true if the given module implements the Probe behaviour. Checks for `probe_init/1`, the one callback every real probe defines (directly or via inheriting `use CodeNameRaven.Probe`'s default). """ @spec probe_module?(module()) :: boolean() def probe_module?(module) do Code.ensure_loaded?(module) and function_exported?(module, :probe_init, 1) end # --------------------------------------------------------------------------- # __using__ — sets up the behaviour and provides default implementations # --------------------------------------------------------------------------- @doc false defmacro __using__(_opts \\ []) do quote do use CodeNameRaven.Monitor @behaviour CodeNameRaven.Probe @impl CodeNameRaven.Monitor def init(params), do: probe_init(params) @impl CodeNameRaven.Monitor def collect(_params, state), do: {:ok, state, state} @impl CodeNameRaven.Probe def probe_init(_params), do: {:ok, %{}} @impl CodeNameRaven.Probe def on_metric(_metric, _params, state), do: {:ok, state} @impl CodeNameRaven.Probe def on_status(_status, _params, state), do: {:ok, state} defoverridable init: 1, collect: 2, probe_init: 1, on_metric: 3, on_status: 3 end end end