Packages
HTTP endpoint monitor, with its dashboard panel bundled in the same package as a separate module (Integrations.Http.Display, #299) — one install, both halves; a release without raven_web simply runs the monitor headless.
Current section
Files
Jump to
Current section
Files
lib/integrations/http/display.ex
defmodule Integrations.Http.Display do
@moduledoc """
Dashboard panel for `Integrations.Http` (same package).
Nested under the monitor's own namespace deliberately — `#299`'s split
pattern: the package's top-level module name (`Integrations.Http`) never
changes, so existing `raven.toml` monitor configs keep resolving
unmodified, and this module's name is guaranteed distinct from the
separately-published `raven_integration_http_display`'s
`Integrations.HttpDisplay` (no dot) — both packages can install and load
simultaneously without colliding.
`Display.BundledDefault` auto-hooks this whenever `Integrations.Http`
starts, the same as if they were still one module — an integrator
bundling Monitor and Display in one package, even across two files, is a
statement of intent that they belong together by default.
"""
use CodeNameRaven.Display, category: :web, size_hint: :full
def display_timezone, do: "America/Chicago"
@impl true
def display_name, do: "HTTP Check"
@impl true
def compatible_monitors, do: [Integrations.Http]
@impl true
def render(assigns) do
window_seconds = assigns.config.display_window_seconds
cutoff = DateTime.add(DateTime.utc_now(), -window_seconds, :second)
sample_key = Map.get(assigns.config, :target_id) || assigns.config.id
samples = CodeNameRaven.Runtime.recent_samples(sample_key, limit: 120)
samples = Enum.filter(samples, fn s -> DateTime.compare(s.collected_at, cutoff) != :lt end)
local_id = CodeNameRaven.Runtime.instance_id()
latest_local = samples |> Enum.filter(&(&1.instance_id == local_id)) |> List.last()
result =
assigns[:result] || (latest_local && %{
status_code: latest_local.status_code,
latency_ms: latest_local.latency_ms,
cert_expiry_days: latest_local.metrics["cert_expiry_days"],
timing: %{
dns_ms: latest_local.metrics["dns_ms"],
tcp_ms: latest_local.metrics["tcp_ms"],
ssl_ms: latest_local.metrics["ssl_ms"],
ttfb_ms: latest_local.metrics["ttfb_ms"],
download_ms: latest_local.metrics["download_ms"]
},
redirects: latest_local.metrics["redirects"]
})
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 || @config.params[:url] || "HTTP Check" %>
</h2>
<p class="text-xs text-base-content/40 font-mono truncate">
<%= @config.params[:url] %>
</p>
</div>
<.status_chip status={@status} />
</div>
<%!-- Vital row --%>
<div :if={@result} class="flex gap-4">
<.stat label="Status" value={to_string(@result.status_code)} />
<.stat label="Latency" value={"#{@result.latency_ms} ms"} />
<div :if={Map.get(@result, :cert_expiry_days)} class="bg-base-200 rounded-lg px-3 py-2 flex-1 text-center">
<p class="text-xs text-base-content/40">Cert Expiry</p>
<p class="text-sm font-mono font-semibold text-success"><%= @result.cert_expiry_days %>d</p>
</div>
<div :if={Map.get(@result, :redirects) && @result.redirects > 0} class="bg-base-200 rounded-lg px-3 py-2 flex-1 text-center">
<p class="text-xs text-base-content/40">Redirects</p>
<p class="text-sm font-mono font-semibold text-base-content"><%= @result.redirects %></p>
</div>
</div>
<%!-- Timing Breakdown --%>
<div :if={@result && @result[:timing] && @result.timing.dns_ms} class="grid grid-cols-5 gap-2 bg-base-200/50 rounded-lg p-2.5 text-center text-xs">
<div>
<span class="text-base-content/40 block">DNS</span>
<span class="font-mono font-semibold"><%= @result.timing.dns_ms %>ms</span>
</div>
<div>
<span class="text-base-content/40 block">TCP</span>
<span class="font-mono font-semibold"><%= @result.timing.tcp_ms %>ms</span>
</div>
<div>
<span class="text-base-content/40 block">SSL</span>
<span class="font-mono font-semibold"><%= if @result.timing.ssl_ms, do: "#{@result.timing.ssl_ms}ms", else: "—" %></span>
</div>
<div>
<span class="text-base-content/40 block">TTFB</span>
<span class="font-mono font-semibold"><%= @result.timing.ttfb_ms %>ms</span>
</div>
<div>
<span class="text-base-content/40 block">Download</span>
<span class="font-mono font-semibold"><%= @result.timing.download_ms %>ms</span>
</div>
</div>
<%!-- Assertions Check --%>
<div :if={@result && Map.has_key?(@result, :assertions) && @result.assertions != []} class="space-y-1">
<h3 class="text-xs font-semibold text-base-content/60">Assertions Check</h3>
<div class="space-y-1">
<%= for assertion <- @result.assertions do %>
<% outcome = CodeNameRaven.Monitor.Assertion.evaluate(assertion, @result) %>
<div class="flex items-center justify-between text-xs p-1.5 rounded bg-base-200/20">
<span class="font-mono truncate max-w-[80%] text-base-content/70">
<%= assertion.type %><%= if assertion.target, do: " [#{assertion.target}]" %> <%= assertion.operator %> <%= inspect(assertion.value) %>
</span>
<%= if outcome == :pass do %>
<span class="text-success font-semibold flex items-center gap-1">✓ Pass</span>
<% else %>
<span class="text-error font-semibold flex items-center gap-1">✗ Fail</span>
<% end %>
</div>
<% end %>
</div>
</div>
<%!-- Error --%>
<p :if={@last_error} class="text-xs text-error font-mono truncate">
<%= @last_error %>
</p>
<CodeNameRaven.MonitorComponents.line_chart
id={"chart-#{@config.id}"}
samples={@samples}
local_id={@local_id}
/>
<%!-- 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 shrink-0">
<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 shrink-0">
<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 shrink-0">
<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 shrink-0">
<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-lg px-3 py-2 flex-1 text-center">
<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
end