diff --git a/app.rb b/app.rb
index 6d18197..8afc537 100644
--- a/app.rb
+++ b/app.rb
@@ -121,7 +121,7 @@ class BingoWeb < Sinatra::Base
[format(t(:dc_intro), name: dc.l(:name, @locale), city: dc.l(:city, @locale), operator: dc.operator),
(format(t(:dc_capacity), mw: mw_prose(dc)) if mw(dc)),
{ 'building' => t(:dc_building), 'planned' => t(:dc_planned) }[dc.status] || (dc.year && format(t(:dc_since), dc.year)),
- format(t(:dc_rank), pos: dc.position)].compact
+ (format(t(:dc_rank), pos: op_rank(dc)) if op_rank(dc))].compact
end
def dc_latest(dc)
@@ -181,6 +181,41 @@ class BingoWeb < Sinatra::Base
def feed_time(i) = Time.utc(i.occurred_on.year, i.occurred_on.month, i.occurred_on.day)
+ # Rank by power among operational datacenters only, the same order as the
+ # default card; nil for sites not yet in service.
+ def op_rank(dc)
+ @op_ranks ||= Bingo::Datacenter.where(status: 'operational').order(:position, :id).select_map(:id)
+ .each_with_index.to_h { |id, i| [id, i + 1] }
+ @op_ranks[dc.id]
+ end
+
+ def probe_json(t)
+ { host: t[:host], port: t[:port], label: t[:label], source_url: t[:source_url],
+ answered: t[:last_ok], rtt_ms: t[:last_rtt_ms] }
+ end
+
+ # WebPage + Dataset (status.json), so the page reads as live open data.
+ def status_ld(report)
+ url = "#{base_url}/#{@locale}/status"
+ JSON.generate(
+ '@context' => 'https://schema.org',
+ '@graph' => [
+ { '@type' => 'WebPage', '@id' => url, 'url' => url, 'name' => t(:status_title), 'description' => @desc,
+ 'inLanguage' => @locale, 'dateModified' => report&.checked_at&.utc&.iso8601,
+ 'isPartOf' => { '@id' => "#{base_url}/#website" },
+ 'breadcrumb' => { '@type' => 'BreadcrumbList', 'itemListElement' => [
+ { '@type' => 'ListItem', 'position' => 1, 'name' => t(:title), 'item' => "#{base_url}/#{@locale}" },
+ { '@type' => 'ListItem', 'position' => 2, 'name' => t(:status_link), 'item' => url }
+ ] } }.compact,
+ { '@type' => 'Dataset', 'name' => t(:status_link), 'description' => t(:status_lead), 'url' => url,
+ 'license' => LICENSE_URL, 'isAccessibleForFree' => true, 'temporalCoverage' => 'P1D',
+ 'creator' => { '@type' => 'Person', 'name' => 'asxpi', 'url' => 'https://asxp.io' },
+ 'distribution' => [{ '@type' => 'DataDownload', 'encodingFormat' => 'application/json',
+ 'contentUrl' => "#{base_url}/status.json" }] }
+ ]
+ ).gsub('', '<\/')
+ end
+
# Bar colour for an answered share (nil = no data).
def avail_class(a)
return '' if a.nil?
@@ -193,9 +228,11 @@ class BingoWeb < Sinatra::Base
{ checked_at: report&.checked_at&.utc&.iso8601, monitor: report&.state,
controls: report&.controls && { ok: report.controls[0], checked: report.controls[1] },
datacenters: (report&.sites || []).map do |s|
- { slug: s.dc.slug, rank: s.dc.position, state: s.state, availability: s.availability&.round(3),
- rtt_ms: s.rtt_ms, targets: s.targets, hourly: s.history.map { |a| a&.round(3) } }
+ { slug: s.dc.slug, rank: s.dc.position, operational_rank: op_rank(s.dc), state: s.state,
+ availability: s.availability&.round(3), rtt_ms: s.rtt_ms, hourly: s.history.map { |a| a&.round(3) },
+ targets: s.targets.map { |t| probe_json(t) } }
end,
+ control_hosts: (report&.control_hosts || []).map { |t| probe_json(t) },
unmonitored: (report&.unmonitored || []).map(&:slug) }
end
@@ -231,7 +268,8 @@ class BingoWeb < Sinatra::Base
content_type 'application/xml'
cache_control :public, max_age: 3600
dcs = Bingo::Datacenter.order(:position, :id).eager(:incidents).all
- @entries = [[->(l) { "/#{l}" }, Bingo::Board.load.updated_on, '/']] +
+ @entries = [[->(l) { "/#{l}" }, Bingo::Board.load.updated_on, '/'],
+ [->(l) { "/#{l}/status" }, nil, '/en/status']] +
dcs.map { |dc| [->(l) { dc_path(dc, l) }, [dc.updated_at, *dc.incidents.map(&:updated_at)].compact.max] }
erb :sitemap, layout: false
end
@@ -280,8 +318,10 @@ class BingoWeb < Sinatra::Base
@locale = locale
@report = status_report
@title = t(:status_title)
- @desc = snippet(t(:status_lead))
+ @desc = t(:status_desc)
@path_for = ->(l) { "/#{l}/status" }
+ @x_default = '/en/status'
+ @ld = status_ld(@report)
cache_control :public, max_age: 60
erb :status
end
@@ -345,7 +385,7 @@ class BingoWeb < Sinatra::Base
'part of the list from the hackyourmom datacenter map (data by Cat Eyes OSINT), re-verified.',
updated_at: Bingo::Board.load.updated_on&.utc&.iso8601,
datacenters: dcs.map do |dc|
- { slug: dc.slug, rank: dc.position, name: loc.(dc, :name), city: loc.(dc, :city),
+ { slug: dc.slug, rank: dc.position, operational_rank: op_rank(dc), name: loc.(dc, :name), city: loc.(dc, :city),
operator: dc.operator, provider: dc.provider, status: dc.status,
mw: dc.mw, mw_value: Bingo.mw_value(dc.mw), mw_type: dc.mw_type, year: dc.year,
info_url: dc.info_url,
diff --git a/lib/bingo.rb b/lib/bingo.rb
index b359ebe..33fc3d5 100644
--- a/lib/bingo.rb
+++ b/lib/bingo.rb
@@ -70,11 +70,11 @@ module Bingo
dc_intro: '%{name} — дата-центр в городе %{city}, оператор — %{operator}.',
dc_capacity: 'Мощность: %{mw}.', dc_building: 'Строится.', dc_planned: 'Планируется.',
dc_since: 'Работает с %d года.',
- dc_rank: '№ %{pos} по мощности среди крупнейших дата-центров России.',
+ dc_rank: '№ %{pos} по мощности среди действующих крупнейших дата-центров России.',
dc_latest: 'Последний инцидент (%{date}): %{title}', incidents: 'Инциденты',
back: '← К карточке', dc_page: 'Страница ЦОДа →',
status_link: 'Доступность ЦОДов', status_title: 'Доступность ЦОДов России — сетевой мониторинг | Бинго ЦОДов',
- status_h1: 'Доступность', status_accent: 'ЦОДов',
+ status_h1: 'Доступность', status_accent: 'ЦОДов', status_tagline: 'Сетевые проверки крупнейших дата-центров России раз в минуту',
status_lead: 'Раз в минуту мы открываем TCP-соединение с публичными адресами, которые открытые источники связывают с конкретной площадкой. Проверка идёт из Финляндии: российская фильтрация и отключения внешних каналов могут выглядеть как авария, поэтому параллельно проверяются контрольные узлы в России.',
status_note: 'Это не подтверждённые инциденты: клетка на карточке зачёркивается только по публичному источнику.',
status_states: { 'up' => 'доступен', 'degraded' => 'частично', 'down' => 'не отвечает', 'unknown' => 'нет данных' },
@@ -84,7 +84,9 @@ module Bingo
status_checked: 'Последняя проверка: %{time} UTC.', status_never: 'Проверок ещё не было.',
status_unmonitored: 'Без мониторинга', status_unmonitored_note: 'Действующие ЦОДы, для которых нет публичных адресов, надёжно привязанных к площадке (обычно собственные ЦОДы Яндекса, VK, Сбера).',
status_cols: { 'dc' => 'ЦОД', 'state' => 'Сейчас', 'hours' => '24 часа', 'rtt' => 'мс', 'targets' => 'адр.' },
- status_dc: 'Доступность сети', status_hour: '%{h} ч назад: %{p}'
+ status_dc: 'Доступность сети', status_hour: '%{h} ч назад: %{p}',
+ status_desc: 'Доступность крупнейших дата-центров России онлайн: ежеминутные сетевые проверки IXcellerate, Selectel, DataPro, Key Point и других, история за 24 часа и открытые данные.',
+ status_probes: 'Что проверяем', status_controls_list: 'Контрольные узлы в России', status_no_answer: 'не отвечает', status_source: 'почему этот адрес'
},
'en' => {
seo_title: 'Datacenter Bingo — strikes, fires and outages at Russian datacenters',
@@ -114,11 +116,11 @@ module Bingo
dc_intro: '%{name} is a datacenter in %{city} operated by %{operator}.',
dc_capacity: 'Capacity: %{mw}.', dc_building: 'Under construction.', dc_planned: 'Planned.',
dc_since: 'In service since %d.',
- dc_rank: "No. %{pos} by power among Russia's largest datacenters.",
+ dc_rank: "No. %{pos} by power among Russia's largest operational datacenters.",
dc_latest: 'Latest reported incident (%{date}): %{title}', incidents: 'Incidents',
back: '← Back to the card', dc_page: 'Datacenter page →',
status_link: 'Datacenter reachability', status_title: "Russian datacenter reachability — network monitor | Datacenter Bingo",
- status_h1: 'Datacenter', status_accent: 'reachability',
+ status_h1: 'Datacenter', status_accent: 'reachability', status_tagline: "Minute-by-minute network checks of Russia's largest datacenters",
status_lead: 'Once a minute we open a TCP connection to public addresses that open sources tie to a specific site. Probes run from Finland: Russian filtering and cuts to international links can look like an outage, so control hosts inside Russia are checked alongside.',
status_note: 'These are not confirmed incidents: a cell on the card is crossed out only on a public source.',
status_states: { 'up' => 'reachable', 'degraded' => 'partial', 'down' => 'not answering', 'unknown' => 'no data' },
@@ -128,7 +130,9 @@ module Bingo
status_checked: 'Last check: %{time} UTC.', status_never: 'No checks yet.',
status_unmonitored: 'Not monitored', status_unmonitored_note: 'Operational datacenters with no public address reliably tied to the site (mostly Yandex, VK and Sber in-house sites).',
status_cols: { 'dc' => 'Datacenter', 'state' => 'Now', 'hours' => '24 hours', 'rtt' => 'ms', 'targets' => 'addr.' },
- status_dc: 'Network reachability', status_hour: '%{h} h ago: %{p}'
+ status_dc: 'Network reachability', status_hour: '%{h} h ago: %{p}',
+ status_desc: "Live reachability of Russia's largest datacenters: minute-by-minute network checks of IXcellerate, Selectel, DataPro, Key Point and others, 24-hour history and open data.",
+ status_probes: 'What we check', status_controls_list: 'Control hosts in Russia', status_no_answer: 'no answer', status_source: 'why this address'
},
'uk' => {
seo_title: 'Бінго ЦОДів — атаки, пожежі та аварії дата-центрів Росії',
@@ -158,11 +162,11 @@ module Bingo
dc_intro: '%{name} — дата-центр у місті %{city}, оператор — %{operator}.',
dc_capacity: 'Потужність: %{mw}.', dc_building: 'Будується.', dc_planned: 'Планується.',
dc_since: 'Працює з %d року.',
- dc_rank: '№ %{pos} за потужністю серед найбільших дата-центрів Росії.',
+ dc_rank: '№ %{pos} за потужністю серед найбільших діючих дата-центрів Росії.',
dc_latest: 'Останній інцидент (%{date}): %{title}', incidents: 'Інциденти',
back: '← До картки', dc_page: 'Сторінка ЦОДу →',
status_link: 'Доступність ЦОДів', status_title: 'Доступність ЦОДів Росії — мережевий моніторинг | Бінго ЦОДів',
- status_h1: 'Доступність', status_accent: 'ЦОДів',
+ status_h1: 'Доступність', status_accent: 'ЦОДів', status_tagline: 'Мережеві перевірки найбільших дата-центрів Росії щохвилини',
status_lead: 'Раз на хвилину ми відкриваємо TCP-з’єднання з публічними адресами, які відкриті джерела пов’язують із конкретним майданчиком. Перевірка йде з Фінляндії: російська фільтрація та відключення зовнішніх каналів можуть виглядати як аварія, тому паралельно перевіряються контрольні вузли в Росії.',
status_note: 'Це не підтверджені інциденти: клітинка на картці закреслюється лише за публічним джерелом.',
status_states: { 'up' => 'доступний', 'degraded' => 'частково', 'down' => 'не відповідає', 'unknown' => 'немає даних' },
@@ -172,7 +176,9 @@ module Bingo
status_checked: 'Остання перевірка: %{time} UTC.', status_never: 'Перевірок ще не було.',
status_unmonitored: 'Без моніторингу', status_unmonitored_note: 'Діючі ЦОДи, для яких немає публічних адрес, надійно прив’язаних до майданчика (здебільшого власні ЦОДи Яндекса, VK, Сбера).',
status_cols: { 'dc' => 'ЦОД', 'state' => 'Зараз', 'hours' => '24 години', 'rtt' => 'мс', 'targets' => 'адр.' },
- status_dc: 'Доступність мережі', status_hour: '%{h} год тому: %{p}'
+ status_dc: 'Доступність мережі', status_hour: '%{h} год тому: %{p}',
+ status_desc: 'Доступність найбільших дата-центрів Росії онлайн: щохвилинні мережеві перевірки IXcellerate, Selectel, DataPro, Key Point та інших, історія за 24 години та відкриті дані.',
+ status_probes: 'Що перевіряємо', status_controls_list: 'Контрольні вузли в Росії', status_no_answer: 'не відповідає', status_source: 'чому ця адреса'
}
}.freeze
@@ -313,8 +319,10 @@ module Bingo
STALE = 900 # newest worker bucket older than this: nothing is current
HOURS = 24
+ # targets / control_hosts: the probed hosts (endpoint, label, source and
+ # last result), shown so readers can see and check what is measured.
Site = Struct.new(:dc, :state, :availability, :rtt_ms, :history, :targets)
- Report = Struct.new(:sites, :unmonitored, :checked_at, :controls, :state) do
+ Report = Struct.new(:sites, :unmonitored, :checked_at, :controls, :state, :control_hosts) do
def counts = sites.map(&:state).tally
end
@@ -355,11 +363,14 @@ module Bingo
bad_hours = buckets.reject { |b| vantage_ok?(b) }.map { |b| hour_index(b[:bucket_at], now, hours) }.to_set
samples = db[:bingo_monitor_samples].where { bucket_at >= since }
- targets = db[:bingo_monitor_targets].where(active: true).exclude(datacenter_id: nil)
+ targets = db[:bingo_monitor_targets].where(active: true)
+ .select(:datacenter_id, :host, :port, :label, :source_url, :last_ok, :last_rtt_ms)
+ .order(:host, :port)
dcs = Datacenter.order(:position, :id)
- samples, targets, dcs = [samples.where(datacenter_id: only), targets.where(datacenter_id: only), dcs.where(id: only)] if only
+ samples, targets, dcs = [samples.where(datacenter_id: only), targets.where(datacenter_id: [only, nil]), dcs.where(id: only)] if only
samples = samples.all.group_by { |r| r[:datacenter_id] }
- targets = targets.group_and_count(:datacenter_id).to_hash(:datacenter_id, :count)
+ targets = targets.all.group_by { |r| r[:datacenter_id] }
+ controls = targets.delete(nil) || []
dcs = dcs.all
monitored, unmonitored = dcs.partition { |dc| targets.key?(dc.id) }
@@ -373,7 +384,7 @@ module Bingo
end
Report.new(sites, unmonitored.select(&:operational?), latest && latest[:bucket_at],
latest && [latest[:control_ok], latest[:control_checks]],
- !fresh ? 'stale' : (trusted ? 'ok' : 'vantage'))
+ !fresh ? 'stale' : (trusted ? 'ok' : 'vantage'), controls)
end
def hour_index(time, now, hours) = hours - 1 - ((now - time) / 3600).floor
diff --git a/test/bingo_test.rb b/test/bingo_test.rb
index ad18d43..f3cf031 100644
--- a/test/bingo_test.rb
+++ b/test/bingo_test.rb
@@ -160,7 +160,8 @@ class BingoTest < Minitest::Test
%w[ru en uk].each { |l| assert_includes last_response.body, " <%= t(:status_states)[@status.state] %> · <%= t(:status_link) %><%= title %> (<%= probes.size %>)
+
+ <% probes.each do |p| %>
+
+<%= p[:host] %>:<%= p[:port] %><% if p[:label] %> — <%= p[:label] %><% end %>
+ <% if p[:last_ok] %> · <%= p[:last_rtt_ms] %> <%= t(:status_cols)['rtt'] %><% elsif p[:last_ok] == false %> · <%= t(:status_no_answer) %><% end %>
+ <% if p[:source_url] %> · <%= t(:status_source) %><% end %>
+ <%= t(:status_dc) %>
<%= t(:status_h1) %> <%= t(:status_accent) %>
+ <%= t(:status_h1) %> <%= t(:status_accent) %> <%= t(:status_tagline) %>
| # | <%= t(:status_cols)['dc'] %> | <%= t(:status_cols)['state'] %> | <%= t(:status_cols)['hours'] %> | -|
|---|---|---|---|---|
| <%= s.dc.position %> | +<%= op_rank(s.dc) %> | <%= s.dc.l(:name, @locale) %><%= s.dc.l(:city, @locale) %> · <%= s.dc.operator %> | <%= t(:status_states)[s.state] %> | <%== erb :_status_bars, layout: false, locals: { history: s.history } %> | -
| <%== erb :_probes, layout: false, locals: { probes: s.targets, title: t(:status_probes) } %> | ||||