Packages

Postgres wire-protocol monitor for connectivity, role, connections, cache efficiency, transaction throughput, and replication health. Includes its dashboard Display module; headless releases run only the Monitor.

Current section

Files

Jump to
raven_integration_postgres lib integrations postgres display.ex
Raw

lib/integrations/postgres/display.ex

defmodule Integrations.Postgres.Display do
@moduledoc """
Dashboard panel for `Integrations.Postgres` (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.Postgres` starts, the same as if they were still one module.
"""
use CodeNameRaven.Display, category: :database, size_hint: :full
@default_port 5432
@default_database "postgres"
@impl true
def display_name, do: "Postgres"
@impl true
def compatible_monitors, do: [Integrations.Postgres]
@impl true
def render(assigns) do
window_seconds = assigns.config.display_window_seconds
cutoff = DateTime.add(DateTime.utc_now(), -window_seconds, :second)
samples = CodeNameRaven.Runtime.recent_samples(assigns.config.id, limit: 120)
samples = Enum.filter(samples, &(DateTime.compare(&1.collected_at, cutoff) != :lt))
local_id = CodeNameRaven.Runtime.instance_id()
result = assigns[:result]
role = result && result.role_metrics
db = result && result.db_metrics
bgw = result && result.bgwriter_metrics
assigns =
Map.merge(assigns, %{
samples: samples,
local_id: local_id,
result: result,
role: role,
db: db,
bgw: bgw
})
~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 || @config.params[:host] || "Postgres" %>
</h2>
<p class="text-xs text-base-content/40 font-mono truncate">
<%= subtitle(@config.params) %>
</p>
</div>
<div class="flex flex-col items-end gap-1 shrink-0">
<.status_chip status={@status} />
<.role_badge :if={@result} result={@result} />
</div>
</div>
<%!-- Row 1: Vital signs --%>
<div class="flex gap-2 flex-wrap">
<.stat label="Latency" value={"#{@result && @result.latency_ms || "–"} ms"} />
<.stat label="Connections" value={conn_label(@role)} />
<.stat label="Idle in Tx" value={"#{(@role && @role.idle_in_transaction) || "–"}"} />
<.stat label="Lock Waiters" value={"#{(@role && @role.waiting_on_lock) || "–"}"} />
</div>
<%!-- Error --%>
<p :if={@last_error} class="text-xs text-error font-mono truncate">
<%= @last_error %>
</p>
<%!-- Row 2: Performance charts --%>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<p class="text-xs text-base-content/40 mb-1">Transactions / sec</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-txrate"}
samples={@samples}
local_id={@local_id}
metric_name="xact_commit_rate"
/>
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Buffer Cache Hit Ratio</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-bhr"}
samples={@samples}
local_id={@local_id}
metric_name="buffer_hit_ratio"
y_max={1.0}
/>
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Tuple Throughput (fetched / sec)</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-tuples"}
samples={@samples}
local_id={@local_id}
metric_name="tup_fetched_rate"
/>
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Deadlocks / sec</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-deadlocks"}
samples={@samples}
local_id={@local_id}
metric_name="deadlocks_rate"
/>
</div>
</div>
<%!-- Row 3: Operational stats --%>
<div class="flex gap-2 flex-wrap">
<.stat label="DB Size" value={format_bytes(@db && @db.db_size_bytes)} />
<.stat label="XID Age" value={format_xid_age(@db && @db.xid_wraparound_age)} />
<.stat label="Checkpoint Req/s" value={format_rate(@bgw && @bgw.checkpoints_req_rate)} />
<.stat label="Buffers Backend/s" value={format_rate(@bgw && @bgw.buffers_backend_rate)} />
</div>
<%!-- Row 4: Legacy latency chart --%>
<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>
<%!-- Footer --%>
<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(assigns) do
~H"""
<span class="badge badge-neutral 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 role_badge(%{result: %{is_replica: false}} = assigns) do
~H"""
<span class="badge badge-success badge-outline text-[10px] px-2 py-2 font-mono font-semibold">
PRIMARY
</span>
"""
end
defp role_badge(%{result: %{is_replica: true}} = assigns) do
~H"""
<span class="badge badge-warning badge-outline text-[10px] px-2 py-2 font-mono font-semibold">
REPLICA
</span>
"""
end
defp role_badge(assigns), do: ~H""
defp stat(assigns) do
~H"""
<div class="bg-base-200 rounded-lg px-3 py-2 flex-1 text-center min-w-[80px]">
<p class="text-xs text-base-content/40"><%= @label %></p>
<p class="text-sm font-mono font-semibold text-base-content"><%= @value %></p>
</div>
"""
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp subtitle(params) do
host = params[:host] || params["host"] || "unknown"
port = params[:port] || params["port"] || @default_port
db = params[:database] || params["database"] || @default_database
"#{host}:#{port}/#{db}"
end
defp conn_label(nil), do: "–"
defp conn_label(%{active_connections: a, max_connections: m}), do: "#{a}/#{m}"
defp conn_label(_), do: "–"
defp format_bytes(nil), do: "–"
defp format_bytes(b) when b >= 1_073_741_824, do: "#{Float.round(b / 1_073_741_824, 1)} GB"
defp format_bytes(b) when b >= 1_048_576, do: "#{Float.round(b / 1_048_576, 1)} MB"
defp format_bytes(b) when b >= 1_024, do: "#{Float.round(b / 1_024, 1)} KB"
defp format_bytes(b), do: "#{b} B"
defp format_xid_age(nil), do: "–"
defp format_xid_age(age) when age >= 1_000_000_000, do: "#{Float.round(age / 1.0e9, 2)}B"
defp format_xid_age(age) when age >= 1_000_000, do: "#{Float.round(age / 1.0e6, 1)}M"
defp format_xid_age(age), do: "#{age}"
defp format_rate(nil), do: "–"
defp format_rate(r) when is_float(r), do: "#{Float.round(r, 2)}/s"
defp format_rate(r), do: "#{r}/s"
end