Packages

Behaviour and use macro for Raven monitor integrations

Current section

Files

Jump to
raven_observer_sdk lib code_name_raven monitor assertion.ex
Raw

lib/code_name_raven/monitor/assertion.ex

defmodule CodeNameRaven.Monitor.Assertion do
@moduledoc """
A generic assertion framework for monitoring integrations.
Allows operators to define configurable health check thresholds.
"""
defstruct [:type, :target, :operator, :value, severity: :down]
@type operator :: :eq | :ne | :lt | :lte | :gt | :gte | :contains | :not_contains | :matches | :in | :present
@type t :: %__MODULE__{
type: String.t(),
target: String.t() | nil,
operator: operator(),
value: term(),
severity: :down | :degraded
}
@type outcome :: :pass | {:fail, :down | :degraded, String.t()}
@operators %{
"eq" => :eq,
"ne" => :ne,
"lt" => :lt,
"lte" => :lte,
"gt" => :gt,
"gte" => :gte,
"contains" => :contains,
"not_contains" => :not_contains,
"matches" => :matches,
"in" => :in,
"present" => :present
}
@doc """
Parses a list of assertion maps (as received from params) into a list of Assertion structs.
"""
@spec parse([map()]) :: [t()]
def parse(list) when is_list(list) do
Enum.map(list, &parse_one/1)
end
def parse(_), do: []
defp parse_one(map) when is_map(map) do
%__MODULE__{
type: map[:type] || map["type"],
target: map[:target] || map["target"],
operator: to_operator(map[:operator] || map["operator"]),
value: map[:value] || map["value"],
severity: to_severity(map[:severity] || map["severity"])
}
end
defp to_operator(nil), do: :eq
defp to_operator(op) when is_atom(op), do: op
defp to_operator(op) when is_binary(op) do
op_lower = String.downcase(op)
Map.get(@operators, op_lower) || try_to_existing_atom(op_lower) || :eq
end
defp to_severity(nil), do: :down
defp to_severity(sev) when is_atom(sev), do: sev
defp to_severity(sev) when is_binary(sev) do
case String.downcase(sev) do
"degraded" -> :degraded
_ -> :down
end
end
@doc """
Evaluates a single assertion against the results map.
"""
@spec evaluate(t(), map()) :: outcome()
def evaluate(%__MODULE__{} = assertion, results) do
case get_actual_value(assertion, results) do
{:ok, actual} ->
if compare(actual, assertion.operator, assertion.value) do
:pass
else
{:fail, assertion.severity, format_failure(assertion, actual)}
end
{:error, reason} ->
{:fail, assertion.severity, reason}
end
end
@doc """
Evaluates all assertions against the results map, returning a combined status
and a list of failure reason messages.
"""
@spec evaluate_all([t()], map()) :: {:up | :degraded | :down, [String.t()]}
def evaluate_all(assertions, results) do
if assertions == [] do
{:up, []}
else
outcomes = Enum.map(assertions, &evaluate(&1, results))
failures =
Enum.flat_map(outcomes, fn
{:fail, _severity, reason} -> [reason]
_ -> []
end)
status =
cond do
Enum.any?(outcomes, fn
{:fail, :down, _} -> true
_ -> false
end) -> :down
Enum.any?(outcomes, fn
{:fail, :degraded, _} -> true
_ -> false
end) -> :degraded
true -> :up
end
{status, failures}
end
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
defp get_actual_value(%__MODULE__{type: type}, results) when type in ["status_code", "status_code_in"] do
val = results[:status_code] || results[:status]
if is_nil(val), do: {:error, "Status code not found in results"}, else: {:ok, val}
end
defp get_actual_value(%__MODULE__{type: type}, results) when type in ["body_contains", "body_matches"] do
{:ok, results[:body] || ""}
end
defp get_actual_value(%__MODULE__{type: type, target: target}, results) when type in ["header_eq", "header_present"] do
headers = results[:headers] || []
found = find_header(headers, target)
if type == "header_eq" do
case found do
{:ok, val} -> {:ok, val}
:error -> {:error, "Header #{inspect(target)} not found"}
end
else
case found do
{:ok, _} -> {:ok, true}
:error -> {:ok, false}
end
end
end
defp get_actual_value(%__MODULE__{type: "cert_expiry_days"}, results) do
val = results[:cert_expiry_days]
if is_nil(val), do: {:error, "Certificate expiry days not found/SSL not used"}, else: {:ok, val}
end
defp get_actual_value(%__MODULE__{type: "latency_ms", target: target}, results) do
get_latency_ms(target, results)
end
defp get_actual_value(%__MODULE__{type: "redirect_count"}, results) do
{:ok, results[:redirects] || 0}
end
defp get_actual_value(assertion, results) do
key = assertion.type
atom_key = try_to_existing_atom(key)
cond do
Map.has_key?(results, key) ->
{:ok, Map.get(results, key)}
atom_key && Map.has_key?(results, atom_key) ->
{:ok, Map.get(results, atom_key)}
val = find_nested_metric(results, key, atom_key) ->
{:ok, val}
true ->
{:error, "Metric key #{inspect(key)} not found in results"}
end
end
defp get_latency_ms(nil, results), do: get_total_latency(results)
defp get_latency_ms("latency_ms", results), do: get_total_latency(results)
defp get_latency_ms("total_ms", results), do: get_total_latency(results)
defp get_latency_ms(target, results) do
timing = results[:timing] || %{}
atom_key = try_to_existing_atom(target)
val = timing[target] || (atom_key && timing[atom_key])
if is_nil(val), do: {:error, "Timing key #{target} not found"}, else: {:ok, val}
end
defp get_total_latency(results) do
val = results[:latency_ms] || results[:total_ms]
if is_nil(val), do: {:error, "Latency not found"}, else: {:ok, val}
end
defp find_nested_metric(results, key, atom_key) do
Enum.find_value(Map.values(results), fn
nested_map when is_map(nested_map) ->
cond do
Map.has_key?(nested_map, key) -> Map.get(nested_map, key)
atom_key && Map.has_key?(nested_map, atom_key) -> Map.get(nested_map, atom_key)
true -> nil
end
_ ->
nil
end)
end
defp compare(actual, :eq, value), do: to_string_or_number(actual) == to_string_or_number(value)
defp compare(actual, :ne, value), do: to_string_or_number(actual) != to_string_or_number(value)
defp compare(actual, :lt, value), do: to_number(actual) < to_number(value)
defp compare(actual, :lte, value), do: to_number(actual) <= to_number(value)
defp compare(actual, :gt, value), do: to_number(actual) > to_number(value)
defp compare(actual, :gte, value), do: to_number(actual) >= to_number(value)
defp compare(actual, :contains, value) do
if is_binary(actual) and is_binary(value) do
String.contains?(actual, value)
else
false
end
end
defp compare(actual, :not_contains, value) do
if is_binary(actual) and is_binary(value) do
not String.contains?(actual, value)
else
true
end
end
defp compare(actual, :matches, value) do
if is_binary(actual) and is_binary(value) do
case Regex.compile(value) do
{:ok, regex} -> Regex.match?(regex, actual)
_ -> false
end
else
false
end
end
defp compare(actual, :in, value) when is_list(value) do
Enum.any?(value, &compare(actual, :eq, &1))
end
defp compare(actual, :present, value) do
# If checking header presence, actual will be a boolean (found or not found)
# value can be true or false
actual == !!value
end
defp compare(_, _, _), do: false
defp find_header(headers, target) when is_list(headers) and is_binary(target) do
target_lower = String.downcase(target)
case Enum.find(headers, fn {k, _v} -> String.downcase(to_string(k)) == target_lower end) do
{_k, v} -> {:ok, to_string(v)}
nil -> :error
end
end
defp find_header(_, _), do: :error
defp to_string_or_number(val) when is_binary(val), do: val
defp to_string_or_number(val) when is_number(val), do: val
defp to_string_or_number(val) when is_atom(val), do: to_string(val)
defp to_string_or_number(val), do: inspect(val)
defp to_number(val) when is_number(val), do: val
defp to_number(val) when is_binary(val) do
case Float.parse(val) do
{num, _} -> num
:error -> 0
end
end
defp to_number(_), do: 0
defp try_to_existing_atom(nil), do: nil
defp try_to_existing_atom(str) when is_binary(str) do
String.to_existing_atom(str)
rescue
_ -> nil
end
defp try_to_existing_atom(atom) when is_atom(atom), do: atom
defp format_failure(assertion, actual) do
target_part = if assertion.target, do: " [#{assertion.target}]", else: ""
"Assertion #{assertion.type}#{target_part} failed: expected #{assertion.operator} #{inspect(assertion.value)}, but got #{inspect(actual)}"
end
end