Packages

RabbitMQ broker monitor, with its dashboard panel bundled in the same package as a separate module (Integrations.RabbitMq.Display) — one install, both halves; a release without raven_web simply runs the monitor headless.

Current section

Files

Jump to
raven_integration_rabbit_mq lib integrations rabbit_mq display.ex
Raw

lib/integrations/rabbit_mq/display.ex

defmodule Integrations.RabbitMq.Display do
@moduledoc """
Dashboard panel for `Integrations.RabbitMq` (same package).
Nested under the monitor's own namespace deliberately — the package's
top-level module name never changes, and this module's name is
guaranteed distinct from any separately-published standalone display
package. `Display.BundledDefault` auto-hooks this whenever
`Integrations.RabbitMq` starts, the same as if they were still one module.
"""
use CodeNameRaven.Display, category: :infrastructure, size_hint: :large
@default_mgmt_port 15672
@impl true
def display_name, do: "RabbitMQ"
@impl true
def compatible_monitors, do: [Integrations.RabbitMq]
@impl true
def render(assigns) do
samples = CodeNameRaven.Runtime.recent_samples(assigns.config.id, limit: 120)
local_id = CodeNameRaven.Runtime.instance_id()
result = assigns[:result]
qt = result && result[:queue_totals] || %{}
ot = result && result[:object_totals] || %{}
ms = result && result[:message_stats] || %{}
node = result && result[:node] || %{}
alarms = result && result[:alarms] || %{}
assigns = Map.merge(assigns, %{
samples: samples,
local_id: local_id,
result: result,
qt: qt,
ot: ot,
ms: ms,
node: node,
alarms: alarms
})
~H"""
<div class="space-y-4">
<%!-- Header --%>
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<h2 class="text-sm font-semibold text-base-content truncate">
<%= @config.name || instance_title(@config.params) %>
</h2>
<p class="text-xs text-base-content/40 font-mono truncate">
<%= instance_title(@config.params) %>
<%= if @result && @result[:version], do: " · v#{@result.version}" %>
<%= if @result && @result[:node_count] && @result.node_count > 1,
do: " · #{@result.node_count} nodes" %>
</p>
</div>
<div class="flex flex-col items-end gap-1 shrink-0">
<.status_chip status={@status} />
<span :if={@alarms[:mem_alarm]} class="badge badge-error badge-sm">mem alarm</span>
<span :if={@alarms[:disk_free_alarm]} class="badge badge-error badge-sm">disk alarm</span>
</div>
</div>
<%!-- Object totals --%>
<div class="flex gap-2 flex-wrap">
<.stat label="Latency" value={"#{(@result && @result.latency_ms) || "–"} ms"} />
<.stat label="Connections" value={"#{@ot[:connections] || "–"}"} />
<.stat label="Queues" value={"#{@ot[:queues] || "–"}"} />
<.stat label="Consumers" value={"#{@ot[:consumers] || "–"}"} />
<.stat label="Channels" value={"#{@ot[:channels] || "–"}"} />
</div>
<%!-- Message depths --%>
<div :if={@qt[:messages]} class="flex gap-2 flex-wrap">
<.stat label="Total Msgs" value={format_count(@qt[:messages])} />
<.stat label="Ready" value={format_count(@qt[:messages_ready])} />
<.stat label="Unacked" value={format_count(@qt[:messages_unacknowledged])} />
</div>
<%!-- Throughput rates --%>
<div :if={@ms[:publish_rate]} class="flex gap-2 flex-wrap">
<.stat label="Publish/s" value={format_rate(@ms[:publish_rate])} />
<.stat label="Deliver/s" value={format_rate(@ms[:deliver_rate])} />
<.stat label="Ack/s" value={format_rate(@ms[:ack_rate])} />
</div>
<%!-- Node resources --%>
<div :if={@node[:mem_used_mb]} class="flex gap-2 flex-wrap">
<.stat label="Mem Used" value={mem_label(@node)} />
<.stat label="FD Used" value={fd_label(@node)} />
<.stat label="Procs" value={proc_label(@node)} />
</div>
<%!-- Charts --%>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<p class="text-xs text-base-content/40 mb-1">Publish / sec</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-pub"}
samples={@samples}
local_id={@local_id}
metric_name="publish_rate" />
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Total Messages</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-msgs"}
samples={@samples}
local_id={@local_id}
metric_name="total_messages" />
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Unacknowledged</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-unacked"}
samples={@samples}
local_id={@local_id}
metric_name="unacked_messages" />
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Latency</p>
<CodeNameRaven.MonitorComponents.line_chart
id={"chart-#{@config.id}-latency"}
samples={@samples}
local_id={@local_id} />
</div>
</div>
<CodeNameRaven.MonitorComponents.error_message message={@last_error} />
<p class="text-xs text-base-content/30">
Checked <%= CodeNameRaven.Timezone.format_datetime(@last_checked_at, __MODULE__) %>
</p>
</div>
"""
end
# ---------------------------------------------------------------------------
# Private components
# ---------------------------------------------------------------------------
defp status_chip(%{status: :up} = assigns) do
~H"""
<span class="badge badge-success gap-1 px-3 py-3 text-xs font-semibold">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Up
</span>
"""
end
defp status_chip(%{status: :degraded} = assigns) do
~H"""
<span class="badge badge-warning gap-1 px-3 py-3 text-xs font-semibold">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Degraded
</span>
"""
end
defp status_chip(%{status: :down} = assigns) do
~H"""
<span class="badge badge-error gap-1 px-3 py-3 text-xs font-semibold">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Down
</span>
"""
end
defp status_chip(%{status: :unknown} = assigns) do
~H"""
<span class="badge badge-ghost gap-1 px-3 py-3 text-xs font-semibold">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Unknown
</span>
"""
end
defp stat(assigns) do
~H"""
<div class="bg-base-200 rounded px-3 py-2 text-center min-w-[70px]">
<p class="text-xs text-base-content/50"><%= @label %></p>
<p class="text-sm font-semibold text-base-content"><%= @value %></p>
</div>
"""
end
# ---------------------------------------------------------------------------
# Render helpers
# ---------------------------------------------------------------------------
defp instance_title(params) do
host = params[:host] || params["host"] || "?"
port = parse_int(params[:management_port] || params["management_port"], @default_mgmt_port)
"#{host}:#{port}"
end
defp mem_label(%{mem_used_mb: used, mem_limit_mb: limit})
when is_integer(used) and is_integer(limit) and limit > 0 do
pct = round(used / limit * 100)
"#{used} MB (#{pct}%)"
end
defp mem_label(%{mem_used_mb: used}) when is_integer(used), do: "#{used} MB"
defp mem_label(_), do: "–"
defp fd_label(%{fd_used: u, fd_total: t}) when is_integer(u) and is_integer(t), do: "#{u}/#{t}"
defp fd_label(%{fd_used: u}) when is_integer(u), do: to_string(u)
defp fd_label(_), do: "–"
defp proc_label(%{proc_used: u, proc_total: t}) when is_integer(u) and is_integer(t), do: "#{u}/#{t}"
defp proc_label(%{proc_used: u}) when is_integer(u), do: to_string(u)
defp proc_label(_), do: "–"
defp format_rate(nil), do: "–"
defp format_rate(r) when is_float(r), do: "#{Float.round(r, 1)}/s"
defp format_rate(r), do: "#{r}/s"
defp format_count(nil), do: "–"
defp format_count(n) when n >= 1_000_000, do: "#{Float.round(n / 1_000_000, 1)}M"
defp format_count(n) when n >= 1_000, do: "#{Float.round(n / 1_000.0, 1)}K"
defp format_count(n), do: to_string(n)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
defp parse_int(nil, default), do: default
defp parse_int(v, _) when is_integer(v), do: v
defp parse_int(v, default) when is_binary(v) do
case Integer.parse(v) do
{n, _} -> n
:error -> default
end
end
defp parse_int(_, default), do: default
end