Rank by operational sites; show what each status check probes
The datacenter text said "No. 33 by power" for a site that is 25th on the default card, because it counted construction projects. Rank among operational datacenters only, matching the card, and leave the rank out for sites not yet in service. data.json and status.json gain an operational_rank next to the list position. The status page now lists, under each site, the hosts it probes with their label, last result and the source that ties them to the site, folded away behind a click; the control hosts in Russia get the same list, and datacenter pages show it under their reachability block. status.json carries the same targets. For search: a keyword title tagline and meta description, WebPage and Dataset structured data (pointing at status.json), x-default, and the status pages in the sitemap. Assisted-by: Claude:opus-5-5
This commit is contained in:
parent
7fb11211d7
commit
5e36cc6a95
8 changed files with 118 additions and 28 deletions
52
app.rb
52
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_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)),
|
(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)),
|
{ '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
|
end
|
||||||
|
|
||||||
def dc_latest(dc)
|
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)
|
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).
|
# Bar colour for an answered share (nil = no data).
|
||||||
def avail_class(a)
|
def avail_class(a)
|
||||||
return '' if a.nil?
|
return '' if a.nil?
|
||||||
|
|
@ -193,9 +228,11 @@ class BingoWeb < Sinatra::Base
|
||||||
{ checked_at: report&.checked_at&.utc&.iso8601, monitor: report&.state,
|
{ checked_at: report&.checked_at&.utc&.iso8601, monitor: report&.state,
|
||||||
controls: report&.controls && { ok: report.controls[0], checked: report.controls[1] },
|
controls: report&.controls && { ok: report.controls[0], checked: report.controls[1] },
|
||||||
datacenters: (report&.sites || []).map do |s|
|
datacenters: (report&.sites || []).map do |s|
|
||||||
{ slug: s.dc.slug, rank: s.dc.position, state: s.state, availability: s.availability&.round(3),
|
{ slug: s.dc.slug, rank: s.dc.position, operational_rank: op_rank(s.dc), state: s.state,
|
||||||
rtt_ms: s.rtt_ms, targets: s.targets, hourly: s.history.map { |a| a&.round(3) } }
|
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,
|
end,
|
||||||
|
control_hosts: (report&.control_hosts || []).map { |t| probe_json(t) },
|
||||||
unmonitored: (report&.unmonitored || []).map(&:slug) }
|
unmonitored: (report&.unmonitored || []).map(&:slug) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -231,7 +268,8 @@ class BingoWeb < Sinatra::Base
|
||||||
content_type 'application/xml'
|
content_type 'application/xml'
|
||||||
cache_control :public, max_age: 3600
|
cache_control :public, max_age: 3600
|
||||||
dcs = Bingo::Datacenter.order(:position, :id).eager(:incidents).all
|
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] }
|
dcs.map { |dc| [->(l) { dc_path(dc, l) }, [dc.updated_at, *dc.incidents.map(&:updated_at)].compact.max] }
|
||||||
erb :sitemap, layout: false
|
erb :sitemap, layout: false
|
||||||
end
|
end
|
||||||
|
|
@ -280,8 +318,10 @@ class BingoWeb < Sinatra::Base
|
||||||
@locale = locale
|
@locale = locale
|
||||||
@report = status_report
|
@report = status_report
|
||||||
@title = t(:status_title)
|
@title = t(:status_title)
|
||||||
@desc = snippet(t(:status_lead))
|
@desc = t(:status_desc)
|
||||||
@path_for = ->(l) { "/#{l}/status" }
|
@path_for = ->(l) { "/#{l}/status" }
|
||||||
|
@x_default = '/en/status'
|
||||||
|
@ld = status_ld(@report)
|
||||||
cache_control :public, max_age: 60
|
cache_control :public, max_age: 60
|
||||||
erb :status
|
erb :status
|
||||||
end
|
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.',
|
'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,
|
updated_at: Bingo::Board.load.updated_on&.utc&.iso8601,
|
||||||
datacenters: dcs.map do |dc|
|
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,
|
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,
|
mw: dc.mw, mw_value: Bingo.mw_value(dc.mw), mw_type: dc.mw_type, year: dc.year,
|
||||||
info_url: dc.info_url,
|
info_url: dc.info_url,
|
||||||
|
|
|
||||||
39
lib/bingo.rb
39
lib/bingo.rb
|
|
@ -70,11 +70,11 @@ module Bingo
|
||||||
dc_intro: '%{name} — дата-центр в городе %{city}, оператор — %{operator}.',
|
dc_intro: '%{name} — дата-центр в городе %{city}, оператор — %{operator}.',
|
||||||
dc_capacity: 'Мощность: %{mw}.', dc_building: 'Строится.', dc_planned: 'Планируется.',
|
dc_capacity: 'Мощность: %{mw}.', dc_building: 'Строится.', dc_planned: 'Планируется.',
|
||||||
dc_since: 'Работает с %d года.',
|
dc_since: 'Работает с %d года.',
|
||||||
dc_rank: '№ %{pos} по мощности среди крупнейших дата-центров России.',
|
dc_rank: '№ %{pos} по мощности среди действующих крупнейших дата-центров России.',
|
||||||
dc_latest: 'Последний инцидент (%{date}): %{title}', incidents: 'Инциденты',
|
dc_latest: 'Последний инцидент (%{date}): %{title}', incidents: 'Инциденты',
|
||||||
back: '← К карточке', dc_page: 'Страница ЦОДа →',
|
back: '← К карточке', dc_page: 'Страница ЦОДа →',
|
||||||
status_link: 'Доступность ЦОДов', status_title: 'Доступность ЦОДов России — сетевой мониторинг | Бинго ЦОДов',
|
status_link: 'Доступность ЦОДов', status_title: 'Доступность ЦОДов России — сетевой мониторинг | Бинго ЦОДов',
|
||||||
status_h1: 'Доступность', status_accent: 'ЦОДов',
|
status_h1: 'Доступность', status_accent: 'ЦОДов', status_tagline: 'Сетевые проверки крупнейших дата-центров России раз в минуту',
|
||||||
status_lead: 'Раз в минуту мы открываем TCP-соединение с публичными адресами, которые открытые источники связывают с конкретной площадкой. Проверка идёт из Финляндии: российская фильтрация и отключения внешних каналов могут выглядеть как авария, поэтому параллельно проверяются контрольные узлы в России.',
|
status_lead: 'Раз в минуту мы открываем TCP-соединение с публичными адресами, которые открытые источники связывают с конкретной площадкой. Проверка идёт из Финляндии: российская фильтрация и отключения внешних каналов могут выглядеть как авария, поэтому параллельно проверяются контрольные узлы в России.',
|
||||||
status_note: 'Это не подтверждённые инциденты: клетка на карточке зачёркивается только по публичному источнику.',
|
status_note: 'Это не подтверждённые инциденты: клетка на карточке зачёркивается только по публичному источнику.',
|
||||||
status_states: { 'up' => 'доступен', 'degraded' => 'частично', 'down' => 'не отвечает', 'unknown' => 'нет данных' },
|
status_states: { 'up' => 'доступен', 'degraded' => 'частично', 'down' => 'не отвечает', 'unknown' => 'нет данных' },
|
||||||
|
|
@ -84,7 +84,9 @@ module Bingo
|
||||||
status_checked: 'Последняя проверка: %{time} UTC.', status_never: 'Проверок ещё не было.',
|
status_checked: 'Последняя проверка: %{time} UTC.', status_never: 'Проверок ещё не было.',
|
||||||
status_unmonitored: 'Без мониторинга', status_unmonitored_note: 'Действующие ЦОДы, для которых нет публичных адресов, надёжно привязанных к площадке (обычно собственные ЦОДы Яндекса, VK, Сбера).',
|
status_unmonitored: 'Без мониторинга', status_unmonitored_note: 'Действующие ЦОДы, для которых нет публичных адресов, надёжно привязанных к площадке (обычно собственные ЦОДы Яндекса, VK, Сбера).',
|
||||||
status_cols: { 'dc' => 'ЦОД', 'state' => 'Сейчас', 'hours' => '24 часа', 'rtt' => 'мс', 'targets' => 'адр.' },
|
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' => {
|
'en' => {
|
||||||
seo_title: 'Datacenter Bingo — strikes, fires and outages at Russian datacenters',
|
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_intro: '%{name} is a datacenter in %{city} operated by %{operator}.',
|
||||||
dc_capacity: 'Capacity: %{mw}.', dc_building: 'Under construction.', dc_planned: 'Planned.',
|
dc_capacity: 'Capacity: %{mw}.', dc_building: 'Under construction.', dc_planned: 'Planned.',
|
||||||
dc_since: 'In service since %d.',
|
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',
|
dc_latest: 'Latest reported incident (%{date}): %{title}', incidents: 'Incidents',
|
||||||
back: '← Back to the card', dc_page: 'Datacenter page →',
|
back: '← Back to the card', dc_page: 'Datacenter page →',
|
||||||
status_link: 'Datacenter reachability', status_title: "Russian datacenter reachability — network monitor | Datacenter Bingo",
|
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_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_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' },
|
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_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_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_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' => {
|
'uk' => {
|
||||||
seo_title: 'Бінго ЦОДів — атаки, пожежі та аварії дата-центрів Росії',
|
seo_title: 'Бінго ЦОДів — атаки, пожежі та аварії дата-центрів Росії',
|
||||||
|
|
@ -158,11 +162,11 @@ module Bingo
|
||||||
dc_intro: '%{name} — дата-центр у місті %{city}, оператор — %{operator}.',
|
dc_intro: '%{name} — дата-центр у місті %{city}, оператор — %{operator}.',
|
||||||
dc_capacity: 'Потужність: %{mw}.', dc_building: 'Будується.', dc_planned: 'Планується.',
|
dc_capacity: 'Потужність: %{mw}.', dc_building: 'Будується.', dc_planned: 'Планується.',
|
||||||
dc_since: 'Працює з %d року.',
|
dc_since: 'Працює з %d року.',
|
||||||
dc_rank: '№ %{pos} за потужністю серед найбільших дата-центрів Росії.',
|
dc_rank: '№ %{pos} за потужністю серед найбільших діючих дата-центрів Росії.',
|
||||||
dc_latest: 'Останній інцидент (%{date}): %{title}', incidents: 'Інциденти',
|
dc_latest: 'Останній інцидент (%{date}): %{title}', incidents: 'Інциденти',
|
||||||
back: '← До картки', dc_page: 'Сторінка ЦОДу →',
|
back: '← До картки', dc_page: 'Сторінка ЦОДу →',
|
||||||
status_link: 'Доступність ЦОДів', status_title: 'Доступність ЦОДів Росії — мережевий моніторинг | Бінго ЦОДів',
|
status_link: 'Доступність ЦОДів', status_title: 'Доступність ЦОДів Росії — мережевий моніторинг | Бінго ЦОДів',
|
||||||
status_h1: 'Доступність', status_accent: 'ЦОДів',
|
status_h1: 'Доступність', status_accent: 'ЦОДів', status_tagline: 'Мережеві перевірки найбільших дата-центрів Росії щохвилини',
|
||||||
status_lead: 'Раз на хвилину ми відкриваємо TCP-з’єднання з публічними адресами, які відкриті джерела пов’язують із конкретним майданчиком. Перевірка йде з Фінляндії: російська фільтрація та відключення зовнішніх каналів можуть виглядати як аварія, тому паралельно перевіряються контрольні вузли в Росії.',
|
status_lead: 'Раз на хвилину ми відкриваємо TCP-з’єднання з публічними адресами, які відкриті джерела пов’язують із конкретним майданчиком. Перевірка йде з Фінляндії: російська фільтрація та відключення зовнішніх каналів можуть виглядати як аварія, тому паралельно перевіряються контрольні вузли в Росії.',
|
||||||
status_note: 'Це не підтверджені інциденти: клітинка на картці закреслюється лише за публічним джерелом.',
|
status_note: 'Це не підтверджені інциденти: клітинка на картці закреслюється лише за публічним джерелом.',
|
||||||
status_states: { 'up' => 'доступний', 'degraded' => 'частково', 'down' => 'не відповідає', 'unknown' => 'немає даних' },
|
status_states: { 'up' => 'доступний', 'degraded' => 'частково', 'down' => 'не відповідає', 'unknown' => 'немає даних' },
|
||||||
|
|
@ -172,7 +176,9 @@ module Bingo
|
||||||
status_checked: 'Остання перевірка: %{time} UTC.', status_never: 'Перевірок ще не було.',
|
status_checked: 'Остання перевірка: %{time} UTC.', status_never: 'Перевірок ще не було.',
|
||||||
status_unmonitored: 'Без моніторингу', status_unmonitored_note: 'Діючі ЦОДи, для яких немає публічних адрес, надійно прив’язаних до майданчика (здебільшого власні ЦОДи Яндекса, VK, Сбера).',
|
status_unmonitored: 'Без моніторингу', status_unmonitored_note: 'Діючі ЦОДи, для яких немає публічних адрес, надійно прив’язаних до майданчика (здебільшого власні ЦОДи Яндекса, VK, Сбера).',
|
||||||
status_cols: { 'dc' => 'ЦОД', 'state' => 'Зараз', 'hours' => '24 години', 'rtt' => 'мс', 'targets' => 'адр.' },
|
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
|
}.freeze
|
||||||
|
|
||||||
|
|
@ -313,8 +319,10 @@ module Bingo
|
||||||
STALE = 900 # newest worker bucket older than this: nothing is current
|
STALE = 900 # newest worker bucket older than this: nothing is current
|
||||||
HOURS = 24
|
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)
|
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
|
def counts = sites.map(&:state).tally
|
||||||
end
|
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
|
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 }
|
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)
|
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] }
|
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
|
dcs = dcs.all
|
||||||
monitored, unmonitored = dcs.partition { |dc| targets.key?(dc.id) }
|
monitored, unmonitored = dcs.partition { |dc| targets.key?(dc.id) }
|
||||||
|
|
||||||
|
|
@ -373,7 +384,7 @@ module Bingo
|
||||||
end
|
end
|
||||||
Report.new(sites, unmonitored.select(&:operational?), latest && latest[:bucket_at],
|
Report.new(sites, unmonitored.select(&:operational?), latest && latest[:bucket_at],
|
||||||
latest && [latest[:control_ok], latest[:control_checks]],
|
latest && [latest[:control_ok], latest[:control_checks]],
|
||||||
!fresh ? 'stale' : (trusted ? 'ok' : 'vantage'))
|
!fresh ? 'stale' : (trusted ? 'ok' : 'vantage'), controls)
|
||||||
end
|
end
|
||||||
|
|
||||||
def hour_index(time, now, hours) = hours - 1 - ((now - time) / 3600).floor
|
def hour_index(time, now, hours) = hours - 1 - ((now - time) / 3600).floor
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,8 @@ class BingoTest < Minitest::Test
|
||||||
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}</loc>" }
|
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}</loc>" }
|
||||||
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}/dc/dc-25</loc>" }
|
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}/dc/dc-25</loc>" }
|
||||||
assert_includes last_response.body, '<xhtml:link rel="alternate" hreflang="uk" href="https://example.org/uk/dc/dc-7"/>'
|
assert_includes last_response.body, '<xhtml:link rel="alternate" hreflang="uk" href="https://example.org/uk/dc/dc-7"/>'
|
||||||
assert_equal 3, last_response.body.scan('hreflang="x-default"').size # card URLs only
|
%w[ru en uk].each { |l| assert_includes last_response.body, "<loc>https://example.org/#{l}/status</loc>" }
|
||||||
|
assert_equal 6, last_response.body.scan('hreflang="x-default"').size # card and status URLs only
|
||||||
assert_includes last_response.body, 'hreflang="x-default"'
|
assert_includes last_response.body, 'hreflang="x-default"'
|
||||||
refute_includes last_response.body, 'changefreq'
|
refute_includes last_response.body, 'changefreq'
|
||||||
assert_match %r{</loc>\n <lastmod>\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ</lastmod>\n <xhtml:link}, last_response.body
|
assert_match %r{</loc>\n <lastmod>\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ</lastmod>\n <xhtml:link}, last_response.body
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@ class StatusTest < Minitest::Test
|
||||||
Bingo::Datacenter.create(slug: "dc-#{s}", position: i + 1, name_ru: "ЦОД #{s}", city_ru: 'Москва',
|
Bingo::Datacenter.create(slug: "dc-#{s}", position: i + 1, name_ru: "ЦОД #{s}", city_ru: 'Москва',
|
||||||
operator: 'Op', status: 'operational')
|
operator: 'Op', status: 'operational')
|
||||||
end
|
end
|
||||||
[@up, @down, @part].each { |dc| @db[:bingo_monitor_targets].insert(datacenter_id: dc.id, host: "h-#{dc.slug}", port: 443, created_at: B) }
|
[@up, @down, @part].each do |dc|
|
||||||
|
@db[:bingo_monitor_targets].insert(datacenter_id: dc.id, host: "h-#{dc.slug}", port: 443, label: "probe #{dc.slug}",
|
||||||
|
source_url: 'https://example.com/src', last_ok: true, last_rtt_ms: 40, created_at: B)
|
||||||
|
end
|
||||||
@db[:bingo_monitor_targets].insert(datacenter_id: nil, host: 'ctl', port: 80, created_at: B)
|
@db[:bingo_monitor_targets].insert(datacenter_id: nil, host: 'ctl', port: 80, created_at: B)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -86,12 +89,26 @@ class StatusTest < Minitest::Test
|
||||||
data = JSON.parse(last_response.body)
|
data = JSON.parse(last_response.body)
|
||||||
assert_equal %w[dc-up dc-down dc-part], data['datacenters'].map { |d| d['slug'] }
|
assert_equal %w[dc-up dc-down dc-part], data['datacenters'].map { |d| d['slug'] }
|
||||||
assert_equal ['dc-none'], data['unmonitored']
|
assert_equal ['dc-none'], data['unmonitored']
|
||||||
|
assert_equal({ 'host' => 'h-dc-up', 'port' => 443, 'label' => 'probe dc-up', 'source_url' => 'https://example.com/src',
|
||||||
|
'answered' => true, 'rtt_ms' => 40 }, data['datacenters'][0]['targets'][0])
|
||||||
|
assert_equal ['ctl'], data['control_hosts'].map { |t| t['host'] }
|
||||||
|
get '/ru/status'
|
||||||
|
assert_includes last_response.body, 'h-dc-part:443'
|
||||||
|
assert_includes last_response.body, '"@type":"Dataset"'
|
||||||
get '/en/dc/dc-up'
|
get '/en/dc/dc-up'
|
||||||
assert_includes last_response.body, 'Network reachability'
|
assert_includes last_response.body, 'Network reachability'
|
||||||
get '/en/dc/dc-none'
|
get '/en/dc/dc-none'
|
||||||
refute_includes last_response.body, 'Network reachability'
|
refute_includes last_response.body, 'Network reachability'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_rank_counts_operational_sites_only
|
||||||
|
@part.update(status: 'building') # position 3 is not in service
|
||||||
|
get '/en/dc/dc-none' # position 4
|
||||||
|
assert_includes last_response.body, 'No. 3 by power among'
|
||||||
|
get '/en/dc/dc-part'
|
||||||
|
refute_includes last_response.body, 'by power among'
|
||||||
|
end
|
||||||
|
|
||||||
def test_status_page_before_any_round
|
def test_status_page_before_any_round
|
||||||
get '/en/status'
|
get '/en/status'
|
||||||
assert last_response.ok?
|
assert last_response.ok?
|
||||||
|
|
|
||||||
13
views/_probes.erb
Normal file
13
views/_probes.erb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<details class="probes">
|
||||||
|
<summary><%= title %> (<%= probes.size %>)</summary>
|
||||||
|
<ul>
|
||||||
|
<% probes.each do |p| %>
|
||||||
|
<li>
|
||||||
|
<span class="st <%= { true => 'up', false => 'down', nil => 'unknown' }[p[:last_ok]] %>"></span>
|
||||||
|
<code><%= p[:host] %>:<%= p[:port] %></code><% 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] %> · <a href="<%= p[:source_url] %>" target="_blank" rel="nofollow noopener"><%= t(:status_source) %></a><% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
<h2><%= t(:status_dc) %></h2>
|
<h2><%= t(:status_dc) %></h2>
|
||||||
<p><span class="st <%= @status.state %>"><%= t(:status_states)[@status.state] %></span> · <a href="/<%= @locale %>/status"><%= t(:status_link) %></a></p>
|
<p><span class="st <%= @status.state %>"><%= t(:status_states)[@status.state] %></span> · <a href="/<%= @locale %>/status"><%= t(:status_link) %></a></p>
|
||||||
<%== erb :_status_bars, layout: false, locals: { history: @status.history } %>
|
<%== erb :_status_bars, layout: false, locals: { history: @status.history } %>
|
||||||
|
<%== erb :_probes, layout: false, locals: { probes: @status.targets, title: t(:status_probes) } %>
|
||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -29,8 +30,8 @@
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<nav class="pager">
|
<nav class="pager">
|
||||||
<% if @prev %><a href="<%= dc_path(@prev) %>" rel="prev">← #<%= @prev.position %> <%= @prev.l(:name, @locale) %></a><% else %><span></span><% end %>
|
<% if @prev %><a href="<%= dc_path(@prev) %>" rel="prev">← <%= "##{op_rank(@prev)} " if op_rank(@prev) %><%= @prev.l(:name, @locale) %></a><% else %><span></span><% end %>
|
||||||
<% if @next %><a href="<%= dc_path(@next) %>" rel="next">#<%= @next.position %> <%= @next.l(:name, @locale) %> →</a><% end %>
|
<% if @next %><a href="<%= dc_path(@next) %>" rel="next"><%= "##{op_rank(@next)} " if op_rank(@next) %><%= @next.l(:name, @locale) %> →</a><% end %>
|
||||||
</nav>
|
</nav>
|
||||||
<p><a href="/<%= @locale %>"><%= t(:back) %></a></p>
|
<p><a href="/<%= @locale %>"><%= t(:back) %></a></p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,13 @@
|
||||||
.bars i.down { background: var(--hit); }
|
.bars i.down { background: var(--hit); }
|
||||||
.status-banner { padding: 10px 14px; border-radius: 8px; background: var(--hit-bg); margin: 0 0 16px; max-width: 65ch; }
|
.status-banner { padding: 10px 14px; border-radius: 8px; background: var(--hit-bg); margin: 0 0 16px; max-width: 65ch; }
|
||||||
.unmonitored { color: var(--muted); font-size: 13px; max-width: 75ch; }
|
.unmonitored { color: var(--muted); font-size: 13px; max-width: 75ch; }
|
||||||
|
.probes { font-size: 13px; color: var(--muted); margin: 4px 0 8px; }
|
||||||
|
.probes summary { cursor: pointer; width: max-content; }
|
||||||
|
.probes ul { list-style: none; margin: 6px 0 0; padding: 0; }
|
||||||
|
.probes li { margin: 2px 0; overflow-wrap: anywhere; }
|
||||||
|
.probes code { color: var(--ink); }
|
||||||
|
.status-table .probe-row td { border-top: 0; padding-top: 0; }
|
||||||
|
.status-table tr:has(+ .probe-row) td { border-bottom: 0; padding-bottom: 2px; }
|
||||||
footer { margin-top: 32px; color: var(--muted); font-size: 13px; }
|
footer { margin-top: 32px; color: var(--muted); font-size: 13px; }
|
||||||
footer p { margin: 0 0 4px; }
|
footer p { margin: 0 0 4px; }
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<% r = @report %>
|
<% r = @report %>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1><%= t(:status_h1) %> <span><%= t(:status_accent) %></span></h1>
|
<h1><%= t(:status_h1) %> <span><%= t(:status_accent) %></span> <small><%= t(:status_tagline) %></small></h1>
|
||||||
<nav class="langs">
|
<nav class="langs">
|
||||||
<% Bingo::LOCALES.each do |l| %>
|
<% Bingo::LOCALES.each do |l| %>
|
||||||
<a href="<%= @path_for.(l) %>" hreflang="<%= l %>" lang="<%= l %>" <%== 'aria-current="page"' if l == @locale %>><%= l %></a>
|
<a href="<%= @path_for.(l) %>" hreflang="<%= l %>" lang="<%= l %>" <%== 'aria-current="page"' if l == @locale %>><%= l %></a>
|
||||||
|
|
@ -24,25 +24,25 @@
|
||||||
<% c = r.counts %>
|
<% c = r.counts %>
|
||||||
<% %w[up degraded down unknown].each do |s| %><% if c[s] %> <span class="st <%= s %>"><%= t(:status_states)[s] %>: <%= c[s] %></span><% end %><% end %>
|
<% %w[up degraded down unknown].each do |s| %><% if c[s] %> <span class="st <%= s %>"><%= t(:status_states)[s] %>: <%= c[s] %></span><% end %><% end %>
|
||||||
</p>
|
</p>
|
||||||
|
<% if r.control_hosts.any? %><%== erb :_probes, layout: false, locals: { probes: r.control_hosts, title: t(:status_controls_list) } %><% end %>
|
||||||
|
|
||||||
<table class="status-table">
|
<table class="status-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th><th><%= t(:status_cols)['dc'] %></th><th><%= t(:status_cols)['state'] %></th>
|
<th>#</th><th><%= t(:status_cols)['dc'] %></th><th><%= t(:status_cols)['state'] %></th>
|
||||||
<th><%= t(:status_cols)['hours'] %></th><th class="num hide-sm"><%= t(:status_cols)['rtt'] %></th>
|
<th><%= t(:status_cols)['hours'] %></th><th class="num hide-sm"><%= t(:status_cols)['rtt'] %></th>
|
||||||
<th class="num hide-sm"><%= t(:status_cols)['targets'] %></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% r.sites.each do |s| %>
|
<% r.sites.each do |s| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="num"><%= s.dc.position %></td>
|
<td class="num"><%= op_rank(s.dc) %></td>
|
||||||
<td><a href="<%= dc_path(s.dc) %>"><%= s.dc.l(:name, @locale) %></a><span class="city"><%= s.dc.l(:city, @locale) %> · <%= s.dc.operator %></span></td>
|
<td><a href="<%= dc_path(s.dc) %>"><%= s.dc.l(:name, @locale) %></a><span class="city"><%= s.dc.l(:city, @locale) %> · <%= s.dc.operator %></span></td>
|
||||||
<td><span class="st <%= s.state %>"><%= t(:status_states)[s.state] %></span></td>
|
<td><span class="st <%= s.state %>"><%= t(:status_states)[s.state] %></span></td>
|
||||||
<td><%== erb :_status_bars, layout: false, locals: { history: s.history } %></td>
|
<td><%== erb :_status_bars, layout: false, locals: { history: s.history } %></td>
|
||||||
<td class="num hide-sm"><%= s.rtt_ms %></td>
|
<td class="num hide-sm"><%= s.rtt_ms %></td>
|
||||||
<td class="num hide-sm"><%= s.targets %></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="probe-row"><td></td><td colspan="4"><%== erb :_probes, layout: false, locals: { probes: s.targets, title: t(:status_probes) } %></td></tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue