CodeNameRaven.Monitor behaviour (raven_observer_sdk v0.5.4)

Copy Markdown

The behaviour contract for a Raven monitor module.

A monitor module is a self-contained plugin. It decides how to collect data, what protocols to use, and how to present results. Raven handles supervision, scheduling, the health signal, and the databus. The monitor handles everything inside those boundaries.

This behaviour is collection-only — no display, no Phoenix, no rendering. CodeNameRaven.Monitor never references Phoenix, even optionally, and never will: a module that uses only this has zero Phoenix dependency and can run on a headless collector node. For the display half, use CodeNameRaven.Display (published separately, in raven_sdk_display) alongside this behaviour.

Implementing a monitor

defmodule MyMonitors.HttpCheck do
  use CodeNameRaven.Monitor
  use CodeNameRaven.Display, category: :web, size_hint: :small

  @impl CodeNameRaven.Monitor
  def collect(%{url: url}, state) do
    case Req.get(url) do
      {:ok, %{status: status}} -> {:ok, %{status: status}, state}
      {:error, reason}         -> {:error, inspect(reason), state}
    end
  end

  @impl CodeNameRaven.Monitor
  def healthy?(%{status: status}) when status in 200..299, do: :up
  def healthy?(_), do: :down
end

collect/2 and healthy?/1 are the only required callbacks. Everything else has a working default. Omit use CodeNameRaven.Display entirely for a collection-only integration — see CodeNameRaven.Display's own moduledoc for the display-only and two-module patterns.

Internal state

The second argument to collect/2 is the monitor's own internal state — opaque to Raven. Use it to carry information between collection cycles: previous readings for delta computation, cached credentials, connection handles. This state is ephemeral: if the process crashes and is restarted, init/1 is called again and state starts fresh.

The health signal

After every collection, Raven calls healthy?/1 with the result and publishes the signal to the databus. This is the only output Raven requires. Everything else — publishing metrics, pushing display data — is the monitor's choice.

Summary

Callbacks

Called by Raven after every completed check cycle — success or failure.

Collects data from the target.

Handles an arbitrary message delivered to the monitor's process.

Determines the health signal from a successful collection result.

Returns the subset of params that defines this monitor's identity.

Initialises the monitor's internal state.

Returns the named numeric metrics produced by a successful collection.

Returns the version of the Raven↔integration contract this module was written against.

Returns the NimbleOptions schema for validating monitor params. Defaults to [] (no pre-flight validation, fully backward compatible).

Returns a template map of params this monitor expects.

Returns the canonical URI that identifies this monitor's target.

Called when the monitor process is shutting down.

Functions

Returns true if module's declared monitor_api_version/0 falls within the range this Raven build supports.

The contract version this running Raven build implements.

The oldest contract version this running Raven build still accepts.

Returns true if the given module implements the Monitor behaviour.

Types

collect_result()

@type collect_result() :: term()

monitor_state()

@type monitor_state() :: term()

status()

@type status() :: :up | :degraded | :down | :unknown

Callbacks

after_check(config, result, status, metrics)

@callback after_check(
  config :: map(),
  result :: collect_result() | nil,
  status :: status(),
  metrics :: map()
) :: :ok

Called by Raven after every completed check cycle — success or failure.

Receives the monitor's config map, the collect result (nil on error), and the resolved health status. Use this hook to persist samples, emit metrics, or trigger any side effect that should happen on every check.

The return value is ignored. Exceptions are caught by the server and logged as warnings — a failing after_check/3 does not affect the monitor's health signal or state.

The default is a no-op. Override in monitors that need persistence:

@impl true
def after_check(%{id: monitor_id}, result, status, metrics) do
  SampleStore.insert(%{monitor_id: monitor_id, status: to_string(status), metrics: metrics, ...})
  :ok
end

collect(params, state)

@callback collect(params :: map(), state :: monitor_state()) ::
  {:ok, collect_result(), monitor_state()}
  | {:error, reason :: String.t(), monitor_state()}

Collects data from the target.

Called on every scheduled check. Receives the monitor's params (from its Config) and its current internal state. Returns the collection result and the (possibly updated) state. The result is passed to healthy?/1 and, for a paired CodeNameRaven.Display, to render/1 as :result in assigns.

Return {:ok, result, new_state} on success or {:error, reason, new_state} on failure. On error, Raven records the failure and increments the failure count; healthy?/1 is not called.

This callback is required — there is no default.

handle_info(msg, params, state)

@callback handle_info(msg :: term(), params :: map(), state :: monitor_state()) ::
  {:ok, monitor_state()} | {:ok, monitor_state(), :check_now}

Handles an arbitrary message delivered to the monitor's process.

Called by Monitor.Server for any message that is not part of the standard check cycle (i.e. not the internal :check timer). Use this to react to external events — for example, subscribing to a DataBus topic in init/1 and processing the arriving messages here.

Return {:ok, new_state} to update the monitor state and wait for the next scheduled check. Return {:ok, new_state, :check_now} to additionally trigger an immediate check cycle — useful when the incoming message contains fresh data that should be evaluated right away rather than waiting up to interval_ms for the next tick.

The default ignores all messages and returns the state unchanged.

healthy?(result)

@callback healthy?(result :: collect_result()) :: status()

Determines the health signal from a successful collection result.

Called after every successful collect/2. The return value is published to the databus as the monitor's health signal.

Return :up if the target is healthy, :down if it is not, or :unknown if the result is ambiguous and a definitive determination cannot be made.

This callback is required — there is no default.

identity_params(params)

@callback identity_params(params :: map()) :: map()

Returns the subset of params that defines this monitor's identity.

The platform hashes {module, identity_params(params)} to produce the monitor id — the key used to register the running process and to store its samples and config. Two starts with the same module and the same identity_params/1 result reconnect to the same history; anything you leave out of the returned map can change freely without forking it.

Use this to separate what makes this a different probe (a different host, a different thing being watched) from incidental configuration (credentials, timeouts, thresholds) that should be tunable without losing history. target_uri/1 already makes this same call for the network-addressable case — if you've implemented it, you likely want identity_params/1 to agree with it:

@impl true
def target_uri(%{url: url}), do: {:ok, url}

@impl true
def identity_params(%{url: url}), do: %{url: url}

The default returns params unchanged — every field is treated as identity-relevant, which is safe but means any change, including incidental ones, forks identity. Override when your params mix identity-essential fields with incidental ones.

init(params)

@callback init(params :: map()) :: {:ok, monitor_state()} | {:error, String.t()}

Initialises the monitor's internal state.

Called once when the monitor process starts, before the first collection tick. Use this to establish connections, seed baseline values, or build any structure the monitor needs to carry between collections.

Returns {:ok, initial_state} on success or {:error, reason} if the monitor cannot start (the supervisor will attempt a restart).

The default implementation returns {:ok, %{}}.

metrics(result)

@callback metrics(result :: collect_result()) :: %{
  required(atom() | String.t()) => number()
}

Returns the named numeric metrics produced by a successful collection.

Called by the platform after every successful collect/2. The returned map is published to the DataBus monitor:metrics topic — one MonitorMetric message per entry. Subscribers (BoundaryProbe, storage writer, alert engine) receive each metric independently.

Keys may be atoms or strings; the platform normalises them to strings before publishing. Values must be numeric (integer or float). Return %{} (the default) if this monitor produces no standalone metrics.

Example:

@impl true
def metrics(%{latency_ms: latency, status_code: code}) do
  %{latency_ms: latency, status_code: code}
end

monitor_api_version()

@callback monitor_api_version() :: pos_integer()

Returns the version of the Raven↔integration contract this module was written against.

This is not the integration's own version — it does not change when you fix a bug or add a param. It changes only when Raven itself changes something about the CodeNameRaven.Monitor behaviour: a callback's signature, a calling convention, what shape Raven passes you. Those changes are rare and deliberate; most Raven releases never touch this.

Raven checks this at load time (boot, or an explicit re-upload) against the range of contract versions the running Raven build supports (CodeNameRaven.Monitor.current_api_version/0). An integration outside that range is rejected and logged rather than loaded — see CodeNameRaven.IntegrationLoader. Already-running monitor processes are never affected by this check; it only applies the next time something is loaded.

The default returns 1, the contract version every integration was implicitly written against before this concept existed. Override only if you are deliberately targeting a specific contract version.

params_schema()

@callback params_schema() :: keyword()

Returns the NimbleOptions schema for validating monitor params. Defaults to [] (no pre-flight validation, fully backward compatible).

params_template()

@callback params_template() :: map()

Returns a template map of params this monitor expects.

Used by the admin UI to decide whether to show a configuration form. If the map is empty the monitor starts immediately with no params. If it is non-empty the UI pre-fills the params form with this template so the operator only needs to fill in the real values.

The default is %{} (no params required). Override this in any monitor that needs configuration — for example:

def params_template, do: %{url: "https://"}

target_uri(params)

@callback target_uri(params :: map()) :: {:ok, String.t()} | :none

Returns the canonical URI that identifies this monitor's target.

The platform hashes this string to produce the target id, which is used to correlate monitors across instances and integrations watching the same asset. Two monitors that return identical strings will be linked to the same target.

Return {:ok, uri} with the canonical URI for the target — preferably the identifier the technology itself defines (a URL, a connection URI, a cloud resource path). Return :none if this monitor has no meaningful external target or if a stable URI cannot be determined from the given params.

The platform does not validate or parse the URI. Consistency is the integration author's responsibility.

The default returns :none. Override in integrations that watch a network-addressable or otherwise canonically identified asset:

@impl true
def target_uri(%{url: url}), do: {:ok, url}

terminate(reason, state)

@callback terminate(reason :: term(), state :: monitor_state()) :: :ok

Called when the monitor process is shutting down.

Use this to release resources opened in init/1 — close connections, flush buffers, etc. The default is a no-op.

Functions

api_version_supported?(module)

@spec api_version_supported?(module()) :: boolean()

Returns true if module's declared monitor_api_version/0 falls within the range this Raven build supports.

Modules that don't export monitor_api_version/0 (predating this check) are treated as version 1, matching the default every integration gets via use CodeNameRaven.Monitor.

current_api_version()

@spec current_api_version() :: pos_integer()

The contract version this running Raven build implements.

min_supported_api_version()

@spec min_supported_api_version() :: pos_integer()

The oldest contract version this running Raven build still accepts.

monitor_module?(module)

@spec monitor_module?(module()) :: boolean()

Returns true if the given module implements the Monitor behaviour.