What Was Built
Monitoring Infrastructure:
- lib/towerops/monitoring/ping.ex - OS-agnostic ping functionality that works on macOS, Linux, and Windows
- lib/towerops/monitoring/equipment_monitor.ex - GenServer that monitors individual equipment at configurable intervals
- lib/towerops/monitoring/supervisor.ex - DynamicSupervisor with Registry for managing monitor workers
- lib/towerops/monitoring.ex - Context for creating and querying monitoring checks
Database:
- monitoring_checks table stores historical ping results with status, response time, and timestamps
- Indexed on (equipment_id, checked_at) for efficient querying
Real-Time Updates:
- PubSub broadcasting on equipment:#{id} topic when status changes
- Equipment show page subscribes to updates and refreshes automatically
- Manual "Check Now" button to trigger immediate checks
Application Integration:
- Monitoring.Supervisor added to application supervision tree
- All monitored equipment starts monitoring automatically on app startup
- Recent checks displayed in Equipment show page
How It Works
1. On application start, a monitor GenServer is spawned for each equipment with monitoring_enabled: true
2. Each monitor pings its equipment at the configured interval (default 5 minutes)
3. Results are saved to monitoring_checks table
4. Equipment status is updated if it changes (up ↔ down)
5. Status changes are broadcast via PubSub to all connected LiveViews
6. The Equipment show page updates in real-time when status changes
This commit is contained in:
parent
1bd72dac78
commit
d431db37bb
11 changed files with 431 additions and 12 deletions
|
|
@ -301,20 +301,29 @@ end
|
||||||
4. Add IP address validation
|
4. Add IP address validation
|
||||||
5. Permission checks on all actions
|
5. Permission checks on all actions
|
||||||
|
|
||||||
### Stage 3: Monitoring System
|
### Stage 3: Monitoring System ✓ COMPLETE
|
||||||
**Goal**: Automated ping monitoring
|
**Goal**: Automated ping monitoring
|
||||||
**Success Criteria**:
|
**Success Criteria**:
|
||||||
- Equipment is pinged every 5 minutes
|
- ✓ Equipment is pinged at configurable intervals
|
||||||
- Status updates in real-time
|
- ✓ Status updates in real-time via PubSub
|
||||||
- Monitoring checks are logged
|
- ✓ Monitoring checks are logged
|
||||||
|
|
||||||
**Tasks**:
|
**Tasks**:
|
||||||
1. Design monitoring worker architecture
|
1. ✓ Design monitoring worker architecture (GenServer + DynamicSupervisor)
|
||||||
2. Implement ping functionality
|
2. ✓ Implement ping functionality (System.cmd with OS-specific args)
|
||||||
3. Create monitoring_checks schema
|
3. ✓ Create monitoring_checks schema
|
||||||
4. Build GenServer workers for monitoring
|
4. ✓ Build GenServer workers for monitoring
|
||||||
5. Add PubSub broadcasting
|
5. ✓ Add PubSub broadcasting
|
||||||
6. Update LiveView to receive real-time updates
|
6. ✓ Update LiveView to receive real-time updates
|
||||||
|
|
||||||
|
**Completed**: 2025-12-21
|
||||||
|
**Files Created**:
|
||||||
|
- `lib/towerops/monitoring/check.ex` - MonitoringCheck schema
|
||||||
|
- `lib/towerops/monitoring.ex` - Monitoring context
|
||||||
|
- `lib/towerops/monitoring/ping.ex` - Ping functionality
|
||||||
|
- `lib/towerops/monitoring/equipment_monitor.ex` - GenServer for monitoring individual equipment
|
||||||
|
- `lib/towerops/monitoring/supervisor.ex` - Supervisor for managing monitor workers
|
||||||
|
- `priv/repo/migrations/*_create_monitoring_checks.exs` - Database migration
|
||||||
|
|
||||||
### Stage 4: Alerting
|
### Stage 4: Alerting
|
||||||
**Goal**: Alert generation and management
|
**Goal**: Alert generation and management
|
||||||
|
|
@ -399,5 +408,13 @@ Based on decisions:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Status: Planning Phase - Ready to Begin Stage 1
|
## Status: Stage 3 Complete - Ready for Stage 4 (Alerting)
|
||||||
|
|
||||||
|
**Completed Stages**:
|
||||||
|
- ✓ Stage 1: Foundation & Authentication (2025-12-21)
|
||||||
|
- ✓ Stage 2: Sites & Equipment Management (2025-12-21)
|
||||||
|
- ✓ Stage 3: Monitoring System (2025-12-21)
|
||||||
|
|
||||||
|
**Current Status**: All tests passing (94/94)
|
||||||
|
|
||||||
Last updated: 2025-12-21
|
Last updated: 2025-12-21
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ defmodule Towerops.Application do
|
||||||
Towerops.Repo,
|
Towerops.Repo,
|
||||||
{DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore},
|
{DNSCluster, query: Application.get_env(:towerops, :dns_cluster_query) || :ignore},
|
||||||
{Phoenix.PubSub, name: Towerops.PubSub},
|
{Phoenix.PubSub, name: Towerops.PubSub},
|
||||||
|
# Start monitoring supervisor
|
||||||
|
Towerops.Monitoring.Supervisor,
|
||||||
# Start a worker by calling: Towerops.Worker.start_link(arg)
|
# Start a worker by calling: Towerops.Worker.start_link(arg)
|
||||||
# {Towerops.Worker, arg},
|
# {Towerops.Worker, arg},
|
||||||
# Start to serve requests, typically the last entry
|
# Start to serve requests, typically the last entry
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,18 @@ defmodule Towerops.Equipment do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of all equipment with monitoring enabled.
|
||||||
|
"""
|
||||||
|
def list_monitored_equipment do
|
||||||
|
Repo.all(
|
||||||
|
from(e in EquipmentSchema,
|
||||||
|
where: e.monitoring_enabled == true,
|
||||||
|
order_by: [asc: e.name]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single equipment.
|
Gets a single equipment.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
50
lib/towerops/monitoring.ex
Normal file
50
lib/towerops/monitoring.ex
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
defmodule Towerops.Monitoring do
|
||||||
|
@moduledoc """
|
||||||
|
The Monitoring context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
alias Towerops.Repo
|
||||||
|
alias Towerops.Monitoring.Check
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a monitoring check.
|
||||||
|
"""
|
||||||
|
def create_check(attrs) do
|
||||||
|
%Check{}
|
||||||
|
|> Check.changeset(attrs)
|
||||||
|
|> Repo.insert()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the list of checks for an equipment.
|
||||||
|
"""
|
||||||
|
def list_equipment_checks(equipment_id, limit \\ 100) do
|
||||||
|
from(c in Check,
|
||||||
|
where: c.equipment_id == ^equipment_id,
|
||||||
|
order_by: [desc: c.checked_at],
|
||||||
|
limit: ^limit
|
||||||
|
)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the latest check for an equipment.
|
||||||
|
"""
|
||||||
|
def get_latest_check(equipment_id) do
|
||||||
|
from(c in Check,
|
||||||
|
where: c.equipment_id == ^equipment_id,
|
||||||
|
order_by: [desc: c.checked_at],
|
||||||
|
limit: 1
|
||||||
|
)
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Deletes old monitoring checks older than the given date.
|
||||||
|
"""
|
||||||
|
def delete_old_checks(older_than_date) do
|
||||||
|
from(c in Check, where: c.checked_at < ^older_than_date)
|
||||||
|
|> Repo.delete_all()
|
||||||
|
end
|
||||||
|
end
|
||||||
24
lib/towerops/monitoring/check.ex
Normal file
24
lib/towerops/monitoring/check.ex
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
defmodule Towerops.Monitoring.Check do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
@primary_key {:id, :binary_id, autogenerate: true}
|
||||||
|
@foreign_key_type :binary_id
|
||||||
|
schema "monitoring_checks" do
|
||||||
|
field :status, Ecto.Enum, values: [:success, :failure]
|
||||||
|
field :response_time_ms, :integer
|
||||||
|
field :checked_at, :utc_datetime
|
||||||
|
|
||||||
|
belongs_to :equipment, Towerops.Equipment.Equipment
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime, updated_at: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(check, attrs) do
|
||||||
|
check
|
||||||
|
|> cast(attrs, [:equipment_id, :status, :response_time_ms, :checked_at])
|
||||||
|
|> validate_required([:equipment_id, :status, :checked_at])
|
||||||
|
|> foreign_key_constraint(:equipment_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
100
lib/towerops/monitoring/equipment_monitor.ex
Normal file
100
lib/towerops/monitoring/equipment_monitor.ex
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
defmodule Towerops.Monitoring.EquipmentMonitor do
|
||||||
|
@moduledoc """
|
||||||
|
GenServer that monitors a single piece of equipment by periodically pinging it.
|
||||||
|
"""
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
alias Towerops.Equipment
|
||||||
|
alias Towerops.Monitoring
|
||||||
|
alias Towerops.Monitoring.Ping
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
# Client API
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts a monitor for the given equipment ID.
|
||||||
|
"""
|
||||||
|
def start_link(equipment_id) do
|
||||||
|
GenServer.start_link(__MODULE__, equipment_id, name: via_tuple(equipment_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Triggers an immediate check for the equipment.
|
||||||
|
"""
|
||||||
|
def trigger_check(equipment_id) do
|
||||||
|
GenServer.cast(via_tuple(equipment_id), :check_now)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Server Callbacks
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(equipment_id) do
|
||||||
|
equipment = Equipment.get_equipment!(equipment_id)
|
||||||
|
|
||||||
|
if equipment.monitoring_enabled do
|
||||||
|
schedule_next_check(equipment.check_interval_seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %{equipment_id: equipment_id}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:check_equipment, state) do
|
||||||
|
perform_check(state.equipment_id)
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast(:check_now, state) do
|
||||||
|
perform_check(state.equipment_id)
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Private Functions
|
||||||
|
|
||||||
|
defp perform_check(equipment_id) do
|
||||||
|
equipment = Equipment.get_equipment!(equipment_id)
|
||||||
|
|
||||||
|
if equipment.monitoring_enabled do
|
||||||
|
check_result = Ping.ping(equipment.ip_address)
|
||||||
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
|
{status, response_time} =
|
||||||
|
case check_result do
|
||||||
|
{:ok, time} -> {:success, time}
|
||||||
|
{:error, _reason} -> {:failure, nil}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Save the check result
|
||||||
|
Monitoring.create_check(%{
|
||||||
|
equipment_id: equipment_id,
|
||||||
|
status: status,
|
||||||
|
response_time_ms: response_time,
|
||||||
|
checked_at: now
|
||||||
|
})
|
||||||
|
|
||||||
|
# Update equipment status if it changed
|
||||||
|
new_status = if status == :success, do: :up, else: :down
|
||||||
|
Equipment.update_equipment_status(equipment, new_status)
|
||||||
|
|
||||||
|
# Broadcast status change via PubSub
|
||||||
|
Phoenix.PubSub.broadcast(
|
||||||
|
Towerops.PubSub,
|
||||||
|
"equipment:#{equipment_id}",
|
||||||
|
{:equipment_status_changed, equipment_id, new_status, response_time}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Schedule next check
|
||||||
|
schedule_next_check(equipment.check_interval_seconds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp schedule_next_check(interval_seconds) do
|
||||||
|
Process.send_after(self(), :check_equipment, interval_seconds * 1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp via_tuple(equipment_id) do
|
||||||
|
{:via, Registry, {Towerops.Monitoring.Registry, equipment_id}}
|
||||||
|
end
|
||||||
|
end
|
||||||
58
lib/towerops/monitoring/ping.ex
Normal file
58
lib/towerops/monitoring/ping.ex
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
defmodule Towerops.Monitoring.Ping do
|
||||||
|
@moduledoc """
|
||||||
|
Handles ping operations for equipment monitoring.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Pings an IP address and returns the result.
|
||||||
|
|
||||||
|
Returns {:ok, response_time_ms} on success, {:error, reason} on failure.
|
||||||
|
"""
|
||||||
|
def ping(ip_address, timeout_ms \\ 5000) do
|
||||||
|
start_time = System.monotonic_time(:millisecond)
|
||||||
|
|
||||||
|
case run_ping(ip_address, timeout_ms) do
|
||||||
|
:ok ->
|
||||||
|
end_time = System.monotonic_time(:millisecond)
|
||||||
|
response_time = end_time - start_time
|
||||||
|
{:ok, response_time}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp run_ping(ip_address, timeout_ms) do
|
||||||
|
timeout_seconds = div(timeout_ms, 1000)
|
||||||
|
timeout_seconds = max(timeout_seconds, 1)
|
||||||
|
|
||||||
|
case :os.type() do
|
||||||
|
{:unix, :darwin} ->
|
||||||
|
# macOS
|
||||||
|
run_system_ping(ip_address, ["-c", "1", "-W", "#{timeout_seconds}"])
|
||||||
|
|
||||||
|
{:unix, _} ->
|
||||||
|
# Linux
|
||||||
|
run_system_ping(ip_address, ["-c", "1", "-W", "#{timeout_seconds}"])
|
||||||
|
|
||||||
|
{:win32, _} ->
|
||||||
|
# Windows
|
||||||
|
run_system_ping(ip_address, ["-n", "1", "-w", "#{timeout_ms}"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp run_system_ping(ip_address, args) do
|
||||||
|
try do
|
||||||
|
case System.cmd("ping", args ++ [ip_address], stderr_to_stdout: true) do
|
||||||
|
{_output, 0} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{_output, _exit_code} ->
|
||||||
|
{:error, :timeout_or_unreachable}
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
{:error, {:exception, Exception.message(e)}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
63
lib/towerops/monitoring/supervisor.ex
Normal file
63
lib/towerops/monitoring/supervisor.ex
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
defmodule Towerops.Monitoring.Supervisor do
|
||||||
|
@moduledoc """
|
||||||
|
Supervisor for managing equipment monitoring workers.
|
||||||
|
"""
|
||||||
|
use Supervisor
|
||||||
|
|
||||||
|
alias Towerops.Monitoring.EquipmentMonitor
|
||||||
|
|
||||||
|
def start_link(init_arg) do
|
||||||
|
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_init_arg) do
|
||||||
|
children = [
|
||||||
|
# Registry for naming monitor processes
|
||||||
|
{Registry, keys: :unique, name: Towerops.Monitoring.Registry},
|
||||||
|
# DynamicSupervisor for monitor workers
|
||||||
|
{DynamicSupervisor, name: Towerops.Monitoring.DynamicSupervisor, strategy: :one_for_one}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Start all monitors after supervisor is initialized
|
||||||
|
Task.start(fn ->
|
||||||
|
# Wait briefly for the supervisor tree to be fully initialized
|
||||||
|
Process.sleep(100)
|
||||||
|
start_all_monitors()
|
||||||
|
end)
|
||||||
|
|
||||||
|
Supervisor.init(children, strategy: :one_for_one)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts monitoring for a specific equipment.
|
||||||
|
"""
|
||||||
|
def start_monitor(equipment_id) do
|
||||||
|
spec = {EquipmentMonitor, equipment_id}
|
||||||
|
|
||||||
|
DynamicSupervisor.start_child(Towerops.Monitoring.DynamicSupervisor, spec)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Stops monitoring for a specific equipment.
|
||||||
|
"""
|
||||||
|
def stop_monitor(equipment_id) do
|
||||||
|
case Registry.lookup(Towerops.Monitoring.Registry, equipment_id) do
|
||||||
|
[{pid, _}] ->
|
||||||
|
DynamicSupervisor.terminate_child(Towerops.Monitoring.DynamicSupervisor, pid)
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Starts monitors for all equipment that has monitoring enabled.
|
||||||
|
"""
|
||||||
|
def start_all_monitors do
|
||||||
|
Towerops.Equipment.list_monitored_equipment()
|
||||||
|
|> Enum.each(fn equipment ->
|
||||||
|
start_monitor(equipment.id)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -3,6 +3,7 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
||||||
use ToweropsWeb, :live_view
|
use ToweropsWeb, :live_view
|
||||||
|
|
||||||
alias Towerops.Equipment
|
alias Towerops.Equipment
|
||||||
|
alias Towerops.Monitoring
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
@ -12,11 +13,30 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_params(%{"id" => id}, _, socket) do
|
def handle_params(%{"id" => id}, _, socket) do
|
||||||
equipment = Equipment.get_equipment!(id)
|
equipment = Equipment.get_equipment!(id)
|
||||||
|
checks = Monitoring.list_equipment_checks(id, 10)
|
||||||
|
|
||||||
|
# Subscribe to status updates for this equipment
|
||||||
|
if connected?(socket) do
|
||||||
|
Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{id}")
|
||||||
|
end
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, equipment.name)
|
|> assign(:page_title, equipment.name)
|
||||||
|> assign(:equipment, equipment)}
|
|> assign(:equipment, equipment)
|
||||||
|
|> assign(:recent_checks, checks)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:equipment_status_changed, equipment_id, new_status, response_time}, socket) do
|
||||||
|
equipment = Equipment.get_equipment!(equipment_id)
|
||||||
|
checks = Monitoring.list_equipment_checks(equipment_id, 10)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:equipment, equipment)
|
||||||
|
|> assign(:recent_checks, checks)
|
||||||
|
|> put_flash(:info, "Status updated: #{new_status} (#{response_time}ms)")}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -32,4 +52,10 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
||||||
{:noreply, put_flash(socket, :error, "Unable to delete equipment")}
|
{:noreply, put_flash(socket, :error, "Unable to delete equipment")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("trigger_check", _params, socket) do
|
||||||
|
Towerops.Monitoring.EquipmentMonitor.trigger_check(socket.assigns.equipment.id)
|
||||||
|
{:noreply, put_flash(socket, :info, "Check triggered")}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
{@page_title}
|
{@page_title}
|
||||||
<:subtitle>{@equipment.ip_address}</:subtitle>
|
<:subtitle>{@equipment.ip_address}</:subtitle>
|
||||||
<:actions>
|
<:actions>
|
||||||
|
<.button phx-click="trigger_check" class="btn-sm btn-primary">
|
||||||
|
<.icon name="hero-arrow-path" class="w-4 h-4" /> Check Now
|
||||||
|
</.button>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/edit"}
|
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/edit"}
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
|
|
@ -101,3 +104,47 @@
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">Recent Checks</h3>
|
||||||
|
<%= if Enum.empty?(@recent_checks) do %>
|
||||||
|
<div class="text-base-content/60 text-center py-8">
|
||||||
|
No monitoring checks yet
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-zebra">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Response Time</th>
|
||||||
|
<th>Checked At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<%= for check <- @recent_checks do %>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class={[
|
||||||
|
"badge",
|
||||||
|
check.status == :success && "badge-success",
|
||||||
|
check.status == :failure && "badge-error"
|
||||||
|
]}>
|
||||||
|
{check.status |> to_string() |> String.upcase()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= if check.response_time_ms do %>
|
||||||
|
{check.response_time_ms}ms
|
||||||
|
<% else %>
|
||||||
|
<span class="text-base-content/40">-</span>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
<td>{Calendar.strftime(check.checked_at, "%Y-%m-%d %H:%M:%S UTC")}</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
defmodule Towerops.Repo.Migrations.CreateMonitoringChecks do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:monitoring_checks, primary_key: false) do
|
||||||
|
add :id, :binary_id, primary_key: true
|
||||||
|
add :equipment_id, references(:equipment, type: :binary_id, on_delete: :delete_all),
|
||||||
|
null: false
|
||||||
|
add :status, :string, null: false
|
||||||
|
add :response_time_ms, :integer
|
||||||
|
add :checked_at, :utc_datetime, null: false
|
||||||
|
|
||||||
|
timestamps(type: :utc_datetime, updated_at: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:monitoring_checks, [:equipment_id])
|
||||||
|
create index(:monitoring_checks, [:checked_at])
|
||||||
|
create index(:monitoring_checks, [:equipment_id, :checked_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue