Packages

OpenSearch (and Elasticsearch-compatible) cluster monitor, with its dashboard panel bundled in the same package as a separate module (Integrations.OpenSearch.Display, #299) — one install, both halves; a release without raven_web simply runs the monitor headless.

Current section

Files

Jump to
raven_integration_open_search lib integrations open_search display.ex
Raw

lib/integrations/open_search/display.ex

defmodule Integrations.OpenSearch.Display do
@moduledoc """
Dashboard panel for `Integrations.OpenSearch` (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.OpenSearch` starts, the same as if they were still one module.
"""
use CodeNameRaven.Display, category: :data, size_hint: :large
@default_port 9200
@impl true
def display_name, do: "OpenSearch"
@impl true
def compatible_monitors, do: [Integrations.OpenSearch]
@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]
assigns = Map.merge(assigns, %{
samples: samples,
local_id: local_id,
result: result
})
~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[:cluster_name], do: " · #{@result.cluster_name}" %>
</p>
</div>
<.cluster_status_badge result={@result} />
</div>
<%!-- Cluster overview --%>
<div :if={@result} class="flex gap-2 flex-wrap">
<.stat label="Latency" value={"#{@result.latency_ms} ms"} />
<.stat label="Nodes" value={"#{@result[:node_count] || "–"}"} />
<.stat label="Indices" value={"#{@result[:index_count] || "–"}"} />
<.stat label="Shards" value={"#{@result[:active_shards] || "–"}"} />
<.stat :if={unassigned?(@result)} label="Unassigned" value={"#{@result[:unassigned_shards]}"} />
<.stat :if={pending?(@result)} label="Pending" value={"#{@result[:pending_tasks]}"} />
</div>
<%!-- Document and store totals --%>
<div :if={@result && @result[:docs_count]} class="flex gap-2 flex-wrap">
<.stat label="Documents" value={format_count(@result[:docs_count])} />
<.stat label="Store Size" value={format_bytes(@result[:store_size_bytes])} />
</div>
<%!-- JVM heap bar --%>
<div :if={@result && is_integer(@result[:heap_used_pct])} class="space-y-1">
<div class="flex justify-between text-xs text-base-content/50">
<span>JVM Heap</span>
<span>
<%= format_bytes(@result[:heap_used_bytes]) %> /
<%= format_bytes(@result[:heap_max_bytes]) %>
(<%= @result[:heap_used_pct] %>%)
</span>
</div>
<div class="w-full bg-base-300 rounded-full h-1.5">
<div
class={"h-1.5 rounded-full #{heap_color(@result[:heap_used_pct])}"}
style={"width: #{min(@result[:heap_used_pct], 100)}%"}>
</div>
</div>
</div>
<%!-- Throughput rates --%>
<div :if={@result && (@result[:indexing_rate] || @result[:search_rate])}
class="flex gap-2 flex-wrap">
<.stat :if={@result[:indexing_rate]} label="Index/s" value={format_rate(@result[:indexing_rate])} />
<.stat :if={@result[:search_rate]} label="Search/s" value={format_rate(@result[:search_rate])} />
</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">Indexing / sec</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-idx"}
samples={@samples}
local_id={@local_id}
metric_name="indexing_rate" />
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Search / sec</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-srch"}
samples={@samples}
local_id={@local_id}
metric_name="search_rate" />
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">JVM Heap %</p>
<CodeNameRaven.MonitorComponents.metric_chart
id={"chart-#{@config.id}-heap"}
samples={@samples}
local_id={@local_id}
metric_name="heap_used_pct"
y_max={100} />
</div>
<div>
<p class="text-xs text-base-content/40 mb-1">Latency</p>
<CodeNameRaven.MonitorComponents.line_chart
id={"chart-#{@config.id}-lat"}
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 cluster_status_badge(%{result: %{cluster_status: "green"}} = assigns) do
~H"""
<span class="badge badge-success gap-1 px-3 py-3 text-xs font-semibold shrink-0">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Green
</span>
"""
end
defp cluster_status_badge(%{result: %{cluster_status: "yellow"}} = assigns) do
~H"""
<span class="badge badge-warning gap-1 px-3 py-3 text-xs font-semibold shrink-0">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Yellow
</span>
"""
end
defp cluster_status_badge(%{result: %{cluster_status: "red"}} = assigns) do
~H"""
<span class="badge badge-error gap-1 px-3 py-3 text-xs font-semibold shrink-0">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Red
</span>
"""
end
defp cluster_status_badge(assigns) do
~H"""
<span class="badge badge-error gap-1 px-3 py-3 text-xs font-semibold shrink-0">
<span class="w-1.5 h-1.5 rounded-full bg-current"></span> Down
</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
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp unassigned?(%{unassigned_shards: n}) when is_integer(n) and n > 0, do: true
defp unassigned?(_), do: false
defp pending?(%{pending_tasks: n}) when is_integer(n) and n > 0, do: true
defp pending?(_), do: false
defp heap_color(pct) when is_integer(pct) and pct >= 90, do: "bg-error"
defp heap_color(pct) when is_integer(pct) and pct >= 75, do: "bg-warning"
defp heap_color(_), do: "bg-success"
defp instance_title(params) do
host = params[:host] || params["host"] || "?"
port = parse_int(params[:port] || params["port"], @default_port)
"#{host}:#{port}"
end
defp format_count(nil), do: "–"
defp format_count(n) when is_number(n) and n >= 1_000_000_000, do: "#{Float.round(n / 1_000_000_000.0, 1)}B"
defp format_count(n) when is_number(n) and n >= 1_000_000, do: "#{Float.round(n / 1_000_000.0, 1)}M"
defp format_count(n) when is_number(n) and n >= 1_000, do: "#{Float.round(n / 1_000.0, 1)}K"
defp format_count(n), do: to_string(n)
defp format_bytes(nil), do: "–"
defp format_bytes(b) when is_number(b) and b >= 1_099_511_627_776, do: "#{Float.round(b / 1_099_511_627_776.0, 1)} TB"
defp format_bytes(b) when is_number(b) and b >= 1_073_741_824, do: "#{Float.round(b / 1_073_741_824.0, 1)} GB"
defp format_bytes(b) when is_number(b) and b >= 1_048_576, do: "#{Float.round(b / 1_048_576.0, 1)} MB"
defp format_bytes(b) when is_number(b) and b >= 1_024, do: "#{Float.round(b / 1_024.0, 1)} KB"
defp format_bytes(b), do: "#{b} B"
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 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