bingo: add a reachability monitor for the status page
datacenterbingo gets a public page showing whether each datacenter's network answers. The probing lives here, next to the schema and the admin, since the public site only has a read-only role. bin/monitor runs as a second container from the same image. Once a minute it opens one TCP connection to every active target (a handshake or a refusal both count as answered) and folds the results into 5-minute rows per datacenter, plus a per-bucket row for control hosts in Russia, so a lost vantage point isn't mistaken for dark sites. Rows are kept 30 days. Targets are managed at /admin/bingo/monitor; only hosts a public source ties to that one site belong there. Migration 017 also grants the new tables to the bingo role, so the public site can read them without a storage redeploy. Assisted-by: Claude:opus-5-5
This commit is contained in:
parent
e0038ef3a4
commit
0cc3e1dece
10 changed files with 415 additions and 1 deletions
60
views/admin/bingo/monitor.erb
Normal file
60
views/admin/bingo/monitor.erb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<%== erb :'admin/_nav', layout: false %>
|
||||
<div class="header">
|
||||
<h1>Bingo monitor</h1>
|
||||
<p class="tagline">
|
||||
<a href="/admin/bingo">← bingo</a>
|
||||
· last round: <%= @last_round ? "#{@last_round[:bucket_at].strftime('%Y-%m-%d %H:%M')} UTC bucket, controls #{@last_round[:control_ok]}/#{@last_round[:control_checks]}" : 'never (is bin/monitor running?)' %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if @flash %><div class="form-error-base"><div><%= @flash %></div></div><% end %>
|
||||
|
||||
<% if can?('bingo', 'write') %>
|
||||
<section class="section">
|
||||
<h2>Add targets</h2>
|
||||
<p class="optional">One per line: <code>slug host[:port] source_url label…</code>. Port defaults to 443. Slug <code><%= Bingo::Monitor::CONTROL %></code> adds a control host (in Russia, not in any listed site). Only hosts a public source ties to that one datacenter; one TCP connect per minute each.</p>
|
||||
<form class="invoice-form" method="post" action="/admin/bingo/monitor" autocomplete="off">
|
||||
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
|
||||
<div class="field"><textarea name="targets" rows="6" style="width: 100%; font-family: monospace" placeholder="selectel-dubrovka-1 203.0.113.10:80 https://example.com/test-ips Selectel test IP ru-3"></textarea></div>
|
||||
<button type="submit" class="download-button">Add</button>
|
||||
</form>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<section class="section">
|
||||
<h2>Targets (<%= @targets.size %>)</h2>
|
||||
<table class="invoice-list">
|
||||
<thead><tr><th>Datacenter</th><th>Endpoint</th><th>Label</th><th>Last</th><th class="num">ms</th><th>Since</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<% @targets.each do |t| %>
|
||||
<tr<%== ' style="opacity: .5"' unless t.active %>>
|
||||
<td><%= t.control? ? 'control' : "##{t.datacenter.position} #{t.datacenter.name_ru}" %></td>
|
||||
<td class="mono"><%= t.endpoint %></td>
|
||||
<td><% if t.source_url %><a href="<%= t.source_url %>" target="_blank" rel="noopener"><%= t.label || 'source' %></a><% else %><%= t.label %><% end %></td>
|
||||
<td><%= { true => '✅', false => '❌', nil => '—' }[t.last_ok] %></td>
|
||||
<td class="num mono"><%= t.last_rtt_ms %></td>
|
||||
<td class="mono"><%= t.last_change_at&.strftime('%m-%d %H:%M') %></td>
|
||||
<td class="actions">
|
||||
<% if can?('bingo', 'write') %>
|
||||
<form method="post" action="/admin/bingo/monitor/<%= t.id %>/toggle">
|
||||
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
|
||||
<button type="submit" class="link-button"><%= t.active ? 'pause' : 'resume' %></button>
|
||||
</form>
|
||||
<% end %>
|
||||
<% if can?('bingo', 'delete') %>
|
||||
<form method="post" action="/admin/bingo/monitor/<%= t.id %>/delete" onsubmit="return confirm('Delete this target?')">
|
||||
<input type="hidden" name="authenticity_token" value="<%= csrf_token %>" />
|
||||
<button type="submit" class="link-button">delete</button>
|
||||
</form>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Operational, not monitored (<%= @unmonitored.size %>)</h2>
|
||||
<p class="optional"><%= @unmonitored.map { |dc| "##{dc.position} #{dc.slug}" }.join(', ') %></p>
|
||||
</section>
|
||||
Loading…
Add table
Add a link
Reference in a new issue