upright 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +208 -56
- data/Rakefile +22 -0
- data/app/assets/images/upright/.keep +0 -0
- data/app/assets/stylesheets/upright/_global.css +2 -0
- data/app/assets/stylesheets/upright/artifact.css +7 -25
- data/app/assets/stylesheets/upright/flash.css +19 -0
- data/app/assets/stylesheets/upright/header.css +14 -0
- data/app/assets/stylesheets/upright/incidents.css +610 -0
- data/app/assets/stylesheets/upright/map.css +6 -0
- data/app/assets/stylesheets/upright/public_status.css +534 -0
- data/app/assets/stylesheets/upright/uptime-bars.css +10 -1
- data/app/controllers/concerns/.keep +0 -0
- data/app/controllers/concerns/upright/authentication.rb +1 -0
- data/app/controllers/concerns/upright/proxy_authentication.rb +134 -0
- data/app/controllers/upright/alertmanager_proxy_controller.rb +12 -4
- data/app/controllers/upright/artifacts_controller.rb +1 -1
- data/app/controllers/upright/incidents/updates_controller.rb +13 -0
- data/app/controllers/upright/incidents_controller.rb +53 -0
- data/app/controllers/upright/probe_results_controller.rb +1 -0
- data/app/controllers/upright/prometheus_proxy_controller.rb +13 -18
- data/app/controllers/upright/public/base_controller.rb +16 -0
- data/app/controllers/upright/public/incidents_controller.rb +6 -0
- data/app/controllers/upright/public/service_incidents_controller.rb +7 -0
- data/app/controllers/upright/public/services_controller.rb +6 -0
- data/app/controllers/upright/sessions_controller.rb +37 -1
- data/app/controllers/upright/traces_controller.rb +88 -0
- data/app/helpers/upright/application_helper.rb +22 -2
- data/app/helpers/upright/artifacts_helper.rb +20 -0
- data/app/helpers/upright/probe_results_helper.rb +4 -10
- data/app/helpers/upright/public/services_helper.rb +70 -0
- data/app/javascript/upright/application.js +3 -0
- data/app/javascript/upright/controllers/sites_map_controller.js +13 -5
- data/app/javascript/upright/controllers/template_controller.js +11 -0
- data/app/javascript/upright/public.js +5 -0
- data/app/jobs/upright/health_metrics_job.rb +27 -0
- data/app/jobs/upright/incident_reporter_job.rb +7 -0
- data/app/jobs/upright/maintenance_advance_job.rb +8 -0
- data/app/jobs/upright/playwright_video_sweep_job.rb +21 -0
- data/app/jobs/upright/rollups/daily_aggregation_job.rb +14 -0
- data/app/models/concerns/.keep +0 -0
- data/app/models/concerns/upright/incidents/auto_reporting.rb +93 -0
- data/app/models/concerns/upright/incidents/lifecycle.rb +47 -0
- data/app/models/concerns/upright/playwright/form_authentication.rb +3 -5
- data/app/models/concerns/upright/playwright/lifecycle.rb +8 -13
- data/app/models/concerns/upright/playwright/trace_recording.rb +53 -0
- data/app/models/concerns/upright/playwright/video_recording.rb +55 -14
- data/app/models/concerns/upright/probe_result/stale_cleanup.rb +47 -0
- data/app/models/concerns/upright/probeable.rb +16 -2
- data/app/models/concerns/upright/services/live_status.rb +69 -0
- data/app/models/concerns/upright/services/maintenance_status.rb +19 -0
- data/app/models/upright/artifact.rb +2 -1
- data/app/models/upright/http/request.rb +13 -4
- data/app/models/upright/incident/automatic_report.rb +3 -0
- data/app/models/upright/incident.rb +87 -0
- data/app/models/upright/incident_affected_service.rb +9 -0
- data/app/models/upright/incident_update.rb +7 -0
- data/app/models/upright/maintenance.rb +36 -0
- data/app/models/upright/persistent_record.rb +12 -0
- data/app/models/upright/playwright/authenticator/base.rb +50 -70
- data/app/models/upright/probe_result.rb +6 -1
- data/app/models/upright/probes/http_probe.rb +30 -1
- data/app/models/upright/probes/playwright/base.rb +2 -0
- data/app/models/upright/probes/status/probe.rb +8 -0
- data/app/models/upright/probes/status.rb +5 -13
- data/app/models/upright/probes/uptime.rb +5 -12
- data/app/models/upright/rollups/daily_uptime.rb +28 -0
- data/app/models/upright/rollups/probe_rollup.rb +64 -0
- data/app/models/upright/rollups/probe_uptime.rb +9 -0
- data/app/models/upright/rollups/site_uptime.rb +75 -0
- data/app/models/upright/service/daily_status.rb +39 -0
- data/app/models/upright/service/incident_history.rb +24 -0
- data/app/models/upright/service/outage.rb +15 -0
- data/app/models/upright/service/status_page.rb +35 -0
- data/app/models/upright/service.rb +101 -0
- data/app/models/upright/status.rb +21 -0
- data/app/models/upright/traceroute/ip_metadata_lookup.rb +21 -2
- data/app/models/upright/traceroute/result.rb +8 -1
- data/app/views/layouts/upright/_header.html.erb +12 -2
- data/app/views/layouts/upright/application.html.erb +4 -1
- data/app/views/layouts/upright/public.html.erb +18 -0
- data/app/views/layouts/upright/public.rss.builder +1 -0
- data/app/views/upright/active_storage/attachments/_attachment.html.erb +1 -1
- data/app/views/upright/artifacts/show.html.erb +8 -0
- data/app/views/upright/dashboards/_uptime_probe_row.html.erb +5 -3
- data/app/views/upright/dashboards/probe_statuses/show.html.erb +1 -1
- data/app/views/upright/dashboards/uptimes/show.html.erb +1 -1
- data/app/views/upright/incidents/_form.html.erb +93 -0
- data/app/views/upright/incidents/_row.html.erb +15 -0
- data/app/views/upright/incidents/edit.html.erb +64 -0
- data/app/views/upright/incidents/index.html.erb +53 -0
- data/app/views/upright/incidents/new.html.erb +6 -0
- data/app/views/upright/probe_results/index.html.erb +7 -4
- data/app/views/upright/public/_branding.html.erb +3 -0
- data/app/views/upright/public/_footer.html.erb +3 -0
- data/app/views/upright/public/incidents/show.html.erb +29 -0
- data/app/views/upright/public/service_incidents/_incident.html.erb +14 -0
- data/app/views/upright/public/service_incidents/index.html.erb +50 -0
- data/app/views/upright/public/services/_active_events.html.erb +28 -0
- data/app/views/upright/public/services/_degraded_list.html.erb +14 -0
- data/app/views/upright/public/services/_overall_banner.html.erb +4 -0
- data/app/views/upright/public/services/_service.html.erb +34 -0
- data/app/views/upright/public/services/_upcoming_maintenance.html.erb +21 -0
- data/app/views/upright/public/services/_uptime_bar.html.erb +9 -0
- data/app/views/upright/public/services/index.html.erb +13 -0
- data/app/views/upright/public/services/index.rss.builder +29 -0
- data/app/views/upright/sessions/new.html.erb +14 -1
- data/config/ci.rb +2 -0
- data/config/importmap.rb +25 -14
- data/config/initializers/date_formats.rb +5 -0
- data/config/initializers/mime_types.rb +1 -0
- data/config/routes.rb +26 -2
- data/db/migrate/20260629000001_fix_probe_result_duration_precision.rb +14 -0
- data/db/persistent_migrate/20260512000001_create_upright_rollups.rb +16 -0
- data/db/persistent_migrate/20260702000001_create_upright_incidents.rb +34 -0
- data/db/persistent_migrate/20260707000001_add_probe_identity_to_upright_rollups.rb +11 -0
- data/db/persistent_migrate/20260709000001_add_created_by_to_incidents.rb +7 -0
- data/db/persistent_migrate/20260901000001_add_auto_reporting_to_upright_incidents.rb +22 -0
- data/lib/generators/upright/install/database_config.rb +46 -0
- data/lib/generators/upright/install/install_generator.rb +52 -3
- data/lib/generators/upright/install/templates/Dockerfile +80 -0
- data/lib/generators/upright/install/templates/content_security_policy.rb +42 -0
- data/lib/generators/upright/install/templates/deploy.yml +13 -13
- data/lib/generators/upright/install/templates/development_prometheus.yml +1 -1
- data/lib/generators/upright/install/templates/docker-compose.yml +12 -11
- data/lib/generators/upright/install/templates/http_probes.yml +1 -0
- data/lib/generators/upright/install/templates/omniauth.rb +7 -3
- data/lib/generators/upright/install/templates/otel_collector.yml +6 -0
- data/lib/generators/upright/install/templates/recurring.yml +45 -0
- data/lib/generators/upright/install/templates/sites.yml +4 -0
- data/lib/generators/upright/install/templates/smtp_probes.yml +3 -0
- data/lib/generators/upright/install/templates/traceroute_probes.yml +3 -0
- data/lib/generators/upright/install/templates/upright.rb +24 -2
- data/lib/generators/upright/install/templates/upright.rules.yml +14 -7
- data/lib/upright/configuration.rb +127 -6
- data/lib/upright/engine.rb +32 -2
- data/lib/upright/metrics.rb +27 -2
- data/lib/upright/probe_type_registry.rb +33 -0
- data/lib/upright/site.rb +31 -1
- data/lib/upright/version.rb +3 -1
- data/lib/upright.rb +34 -2
- metadata +110 -7
- data/config/credentials/development.key +0 -1
- data/config/credentials/test.key +0 -1
- data/lib/tasks/upright_tasks.rake +0 -4
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<% @page_title = "Incidents and maintenance" %>
|
|
2
|
+
|
|
3
|
+
<div class="incidents-index">
|
|
4
|
+
<header class="incidents-header">
|
|
5
|
+
<h1 class="incidents-header__title">Incidents & maintenance</h1>
|
|
6
|
+
<div class="incidents-actions">
|
|
7
|
+
<%= link_to "New incident", new_incident_path, class: "btn" %>
|
|
8
|
+
<%= link_to "New maintenance", new_incident_path(maintenance: true), class: "btn btn--ghost" %>
|
|
9
|
+
</div>
|
|
10
|
+
</header>
|
|
11
|
+
|
|
12
|
+
<% if @current.none? && @upcoming.none? && @past.none? %>
|
|
13
|
+
<div class="incidents-empty">
|
|
14
|
+
<p class="incidents-empty__headline">All clear</p>
|
|
15
|
+
<p class="incidents-empty__body">Nothing to report. Incidents and maintenance you declare here appear on the public status page.</p>
|
|
16
|
+
</div>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if @current.any? %>
|
|
20
|
+
<section class="incident-section">
|
|
21
|
+
<h2 class="incident-section__title">Active</h2>
|
|
22
|
+
<div class="incident-list">
|
|
23
|
+
<% @current.each do |incident| %>
|
|
24
|
+
<%= render "row", incident: incident,
|
|
25
|
+
when_label: incident.maintenance? ? maintenance_window_description(incident) : "Started #{incident.starts_at.to_fs(:month_day_at_zone)}" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<% if @upcoming.any? %>
|
|
32
|
+
<section class="incident-section">
|
|
33
|
+
<h2 class="incident-section__title">Upcoming maintenance</h2>
|
|
34
|
+
<div class="incident-list">
|
|
35
|
+
<% @upcoming.each do |incident| %>
|
|
36
|
+
<%= render "row", incident: incident, when_label: maintenance_window_description(incident) %>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<% if @past.any? %>
|
|
43
|
+
<section class="incident-section">
|
|
44
|
+
<h2 class="incident-section__title">Past</h2>
|
|
45
|
+
<div class="incident-list">
|
|
46
|
+
<% @past.each do |incident| %>
|
|
47
|
+
<%= render "row", incident: incident,
|
|
48
|
+
when_label: "Resolved #{incident.resolved_at&.to_fs(:month_day_at_zone)}" %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
<aside>
|
|
3
3
|
<ul>
|
|
4
4
|
<li><%= type_filter_link "All" %></li>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<li><%= type_filter_link "Traceroute", "traceroute" %></li>
|
|
5
|
+
<% Upright.probe_types.each do |probe_type| %>
|
|
6
|
+
<li><%= type_filter_link probe_type.name, probe_type.type %></li>
|
|
7
|
+
<% end %>
|
|
9
8
|
</ul>
|
|
10
9
|
</aside>
|
|
11
10
|
|
|
@@ -16,6 +15,7 @@
|
|
|
16
15
|
<div class="filters">
|
|
17
16
|
<%= form_with url: site_root_path, method: :get, data: { controller: "form" } do |f| %>
|
|
18
17
|
<%= hidden_field_tag :probe_type, params[:probe_type] if params[:probe_type].present? %>
|
|
18
|
+
<%= hidden_field_tag :date, params[:date] if params[:date].present? %>
|
|
19
19
|
<label for="probe_name">Name</label>
|
|
20
20
|
<%= select_tag :probe_name,
|
|
21
21
|
options_for_select([["All", ""]] + @probe_names, params[:probe_name]),
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
options_for_select([["All", ""]] + Upright::ProbeResult.statuses.keys.map { |s| [s.titleize, s] }, params[:status]),
|
|
27
27
|
class: "input--select",
|
|
28
28
|
data: { action: "change->form#submit" } %>
|
|
29
|
+
<% if params[:date].present? %>
|
|
30
|
+
<%= link_to "Clear date filter", site_root_path(probe_type: params[:probe_type], probe_name: params[:probe_name], status: params[:status]), class: "filter-clear" %>
|
|
31
|
+
<% end %>
|
|
29
32
|
<% end %>
|
|
30
33
|
</div>
|
|
31
34
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<% @page_title = @incident.title %>
|
|
2
|
+
|
|
3
|
+
<article class="incident-detail incident-detail--<%= @incident.maintenance? ? "maintenance" : @incident.impact %>">
|
|
4
|
+
<%= link_to "← Status", "/", class: "incident-detail__back" %>
|
|
5
|
+
|
|
6
|
+
<header class="incident-detail__head">
|
|
7
|
+
<p class="incident-detail__eyebrow"><%= @incident.maintenance? ? "Scheduled maintenance" : "Incident" %></p>
|
|
8
|
+
<h1 class="incident-detail__title"><%= @incident.title %></h1>
|
|
9
|
+
<div class="incident-detail__meta">
|
|
10
|
+
<span class="incident-detail__status"><%= status_label(@incident.status) %></span>
|
|
11
|
+
<% if @incident.maintenance? %>
|
|
12
|
+
<span><%= local_maintenance_window(@incident) %></span>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% if @incident.public_services.any? %>
|
|
15
|
+
<span><%= @incident.public_services.map(&:name).to_sentence %></span>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<ol class="incident-detail__timeline">
|
|
21
|
+
<% @incident.updates.each do |update| %>
|
|
22
|
+
<li class="incident-detail__update">
|
|
23
|
+
<span class="incident-detail__update-status"><%= status_label(update.status) %></span>
|
|
24
|
+
<%= local_time update.created_at, format: "%b %-d, %Y · %H:%M %Z", class: "incident-detail__update-time" %>
|
|
25
|
+
<% if update.body.present? %><p class="incident-detail__update-body"><%= update.body %></p><% end %>
|
|
26
|
+
</li>
|
|
27
|
+
<% end %>
|
|
28
|
+
</ol>
|
|
29
|
+
</article>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%= link_to public_incident_path(incident), class: "service-history__incident service-history__incident--#{incident.impact}" do %>
|
|
2
|
+
<div class="service-history__incident-head">
|
|
3
|
+
<h3 class="service-history__incident-title"><%= incident.title %></h3>
|
|
4
|
+
<span class="service-history__incident-impact"><%= incident.impact.humanize %></span>
|
|
5
|
+
</div>
|
|
6
|
+
<p class="service-history__incident-dates">
|
|
7
|
+
<%= local_time incident.starts_at, format: "%b %-d, %Y · %H:%M %Z" %>
|
|
8
|
+
—
|
|
9
|
+
<%= incident.resolved_at? ? local_time(incident.resolved_at, format: "%b %-d, %Y · %H:%M %Z") : "Ongoing" %>
|
|
10
|
+
</p>
|
|
11
|
+
<% if incident.updates.first&.body.present? %>
|
|
12
|
+
<p class="service-history__incident-update"><%= incident.updates.first.body %></p>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<% @page_title = @incident_history.name %>
|
|
2
|
+
<% uptime_history = @incident_history.daily_status_history %>
|
|
3
|
+
<% today_status = uptime_history.last.status %>
|
|
4
|
+
|
|
5
|
+
<article class="service-detail">
|
|
6
|
+
<%= link_to "← Status", public_services_root_path, class: "service-detail__back" %>
|
|
7
|
+
|
|
8
|
+
<header class="service-detail__head">
|
|
9
|
+
<div class="service__row">
|
|
10
|
+
<h1 class="service-detail__title"><%= @incident_history.name %></h1>
|
|
11
|
+
<% if @incident_history.maintenance_active? %>
|
|
12
|
+
<span class="service__status service__status--maintenance">Under maintenance</span>
|
|
13
|
+
<% else %>
|
|
14
|
+
<span class="service__status service__status--<%= today_status %>"><%= status_label(today_status) %></span>
|
|
15
|
+
<% end %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="uptime">
|
|
19
|
+
<div class="uptime__bars" role="img" aria-label="<%= uptime_history.size %>-day uptime for <%= @incident_history.name %>">
|
|
20
|
+
<%= render partial: "upright/public/services/uptime_bar", collection: uptime_history, as: :day %>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="uptime__meta">
|
|
23
|
+
<span><%= uptime_history.size %> days ago</span>
|
|
24
|
+
<% if uptime = uptime_percentage_label(uptime_history.map(&:uptime_fraction).compact) %>
|
|
25
|
+
<span class="uptime__meta-percent"><%= uptime %> uptime</span>
|
|
26
|
+
<% else %>
|
|
27
|
+
<span class="uptime__meta-percent">No data yet</span>
|
|
28
|
+
<% end %>
|
|
29
|
+
<span>Today</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</header>
|
|
33
|
+
|
|
34
|
+
<% if @incident_history.active_incidents.any? %>
|
|
35
|
+
<section class="service-history">
|
|
36
|
+
<h2 class="service-history__title">Active incidents</h2>
|
|
37
|
+
<%= render partial: "incident", collection: @incident_history.active_incidents %>
|
|
38
|
+
</section>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<section class="service-history">
|
|
42
|
+
<h2 class="service-history__title">Incident history</h2>
|
|
43
|
+
<% if @incident_history.past_incidents.any? %>
|
|
44
|
+
<%= render partial: "incident", collection: @incident_history.past_incidents %>
|
|
45
|
+
<%= render "upright/probe_results/pagination", page: @incident_history.page %>
|
|
46
|
+
<% else %>
|
|
47
|
+
<p class="service-history__empty">No past incidents.</p>
|
|
48
|
+
<% end %>
|
|
49
|
+
</section>
|
|
50
|
+
</article>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<% events = incidents.to_a + maintenances.to_a %>
|
|
2
|
+
<% if events.any? %>
|
|
3
|
+
<section class="events">
|
|
4
|
+
<% events.each do |event| %>
|
|
5
|
+
<%= link_to public_incident_path(event), class: "event event--#{event.maintenance? ? "maintenance" : event.impact}" do %>
|
|
6
|
+
<div class="event__head">
|
|
7
|
+
<span class="event__title"><%= event.title %></span>
|
|
8
|
+
<span class="event__status"><%= status_label(event.status) %></span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if event.maintenance? %>
|
|
12
|
+
<p class="event__meta"><%= local_maintenance_window(event) %></p>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<% if event.public_services.any? %>
|
|
16
|
+
<p class="event__services"><%= event.public_services.map(&:name).to_sentence %></p>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<% if update = event.updates.first %>
|
|
20
|
+
<p class="event__update">
|
|
21
|
+
<%= update.body %>
|
|
22
|
+
<%= local_time update.created_at, format: :month_day_at_zone, class: "event__time" %>
|
|
23
|
+
</p>
|
|
24
|
+
<% end %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</section>
|
|
28
|
+
<% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<% if degraded.any? %>
|
|
2
|
+
<ul class="status-degraded">
|
|
3
|
+
<% degraded.each do |outage| %>
|
|
4
|
+
<li class="status-degraded__item">
|
|
5
|
+
<span class="status-degraded__dot status-degraded__dot--<%= outage.status %>" aria-hidden="true"></span>
|
|
6
|
+
<span class="status-degraded__name"><%= outage.service.name %></span>
|
|
7
|
+
<span class="status-degraded__detail">
|
|
8
|
+
<%= status_label(outage.status).downcase %>
|
|
9
|
+
<% if outage.started_at %>· <%= local_time_ago outage.started_at %><% end %>
|
|
10
|
+
</span>
|
|
11
|
+
</li>
|
|
12
|
+
<% end %>
|
|
13
|
+
</ul>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<% today_status = history.last.status %>
|
|
2
|
+
|
|
3
|
+
<section class="service">
|
|
4
|
+
<div class="service__row">
|
|
5
|
+
<%= link_to service.name, public_service_incidents_path(service.code), class: "service__name" %>
|
|
6
|
+
<% if service.maintenance_active? %>
|
|
7
|
+
<span class="service__status service__status--maintenance">Under maintenance</span>
|
|
8
|
+
<% else %>
|
|
9
|
+
<span class="service__status service__status--<%= today_status %>">
|
|
10
|
+
<%= status_label(today_status) %>
|
|
11
|
+
</span>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<% if description = service.try(:description).presence %>
|
|
16
|
+
<p class="service__description"><%= description %></p>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<div class="uptime">
|
|
20
|
+
<div class="uptime__bars" role="img" aria-label="<%= history.size %>-day uptime for <%= service.name %>">
|
|
21
|
+
<%= render partial: "uptime_bar", collection: history, as: :day %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="uptime__meta">
|
|
25
|
+
<span><%= history.size %> days ago</span>
|
|
26
|
+
<% if uptime = uptime_percentage_label(history.map(&:uptime_fraction).compact) %>
|
|
27
|
+
<span class="uptime__meta-percent"><%= uptime %> uptime</span>
|
|
28
|
+
<% else %>
|
|
29
|
+
<span class="uptime__meta-percent">No data yet</span>
|
|
30
|
+
<% end %>
|
|
31
|
+
<span>Today</span>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<% if maintenances.any? %>
|
|
2
|
+
<section class="upcoming">
|
|
3
|
+
<h2 class="upcoming__title">Scheduled maintenance</h2>
|
|
4
|
+
<% maintenances.each do |maintenance| %>
|
|
5
|
+
<%= link_to public_incident_path(maintenance), class: "upcoming__item" do %>
|
|
6
|
+
<div class="event__head">
|
|
7
|
+
<span class="event__title"><%= maintenance.title %></span>
|
|
8
|
+
<span class="upcoming__when"><%= local_maintenance_window(maintenance) %></span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<% if maintenance.public_services.any? %>
|
|
12
|
+
<p class="event__services"><%= maintenance.public_services.map(&:name).to_sentence %></p>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<% if (update = maintenance.updates.first) && update.body.present? %>
|
|
16
|
+
<p class="event__update"><%= update.body %></p>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
</section>
|
|
21
|
+
<% end %>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<span class="uptime__bar uptime__bar--<%= day.status %>" aria-label="<%= day.aria_label %>">
|
|
2
|
+
<span class="uptime__tooltip" aria-hidden="true">
|
|
3
|
+
<span class="uptime__tooltip-date"><%= day.date_label %></span>
|
|
4
|
+
<span class="uptime__tooltip-detail">
|
|
5
|
+
<span class="uptime__tooltip-dot uptime__tooltip-dot--<%= day.status || "none" %>"></span>
|
|
6
|
+
<%= day.detail %>
|
|
7
|
+
</span>
|
|
8
|
+
</span>
|
|
9
|
+
</span>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="status-page">
|
|
2
|
+
<h1 class="status-page__title">Status</h1>
|
|
3
|
+
|
|
4
|
+
<%= render "overall_banner", status: @status_page.overall_status %>
|
|
5
|
+
<%= render "degraded_list", degraded: @status_page.outages %>
|
|
6
|
+
|
|
7
|
+
<%= render "active_events", incidents: @status_page.active_incidents, maintenances: @status_page.active_maintenances %>
|
|
8
|
+
<%= render "upcoming_maintenance", maintenances: @status_page.upcoming_maintenances %>
|
|
9
|
+
|
|
10
|
+
<% @status_page.service_histories.each do |service, history| %>
|
|
11
|
+
<%= render "service", service: service, history: history %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
2
|
+
xml.rss(version: "2.0") do
|
|
3
|
+
xml.channel do
|
|
4
|
+
xml.title "Upright Status"
|
|
5
|
+
xml.link request.base_url
|
|
6
|
+
xml.description "Currently degraded services"
|
|
7
|
+
xml.lastBuildDate Time.current.rfc822
|
|
8
|
+
|
|
9
|
+
@status_page.outages.each do |outage|
|
|
10
|
+
xml.item do
|
|
11
|
+
xml.title "#{outage.service.name} — #{status_label(outage.status)}"
|
|
12
|
+
xml.description "#{outage.service.name} is currently #{status_label(outage.status).downcase} #{outage_duration_description(started_at: outage.started_at)}."
|
|
13
|
+
xml.pubDate outage.started_at.rfc822 if outage.started_at
|
|
14
|
+
xml.guid feed_item_guid(outage), isPermaLink: "false"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@status_page.active_events.each do |event|
|
|
19
|
+
update = event.updates.first
|
|
20
|
+
xml.item do
|
|
21
|
+
xml.title "#{event.title} — #{status_label(event.status)}"
|
|
22
|
+
xml.description update&.body.to_s
|
|
23
|
+
xml.link public_incident_url(event)
|
|
24
|
+
xml.pubDate (update&.created_at || event.starts_at).rfc822
|
|
25
|
+
xml.guid "incident-#{event.id}-#{update&.id}", isPermaLink: "false"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
<div class="sign-in-container">
|
|
2
2
|
<div class="sign-in-box">
|
|
3
3
|
<h1>Upright</h1>
|
|
4
|
-
|
|
4
|
+
<% if Upright.configuration.auth_provider.to_sym == :static_credentials %>
|
|
5
|
+
<%# Post credentials straight to the OmniAuth callback, but as a same-origin
|
|
6
|
+
form carrying a Rails authenticity token so a cross-site request can't
|
|
7
|
+
forge a sign-in (CVE-2026-67993). %>
|
|
8
|
+
<%= form_with url: "/auth/static_credentials/callback", method: :post, data: { turbo: false } do |form| %>
|
|
9
|
+
<%= form.label :username, "Username" %>
|
|
10
|
+
<%= form.text_field :username, autocomplete: "username" %>
|
|
11
|
+
<%= form.label :password, "Password" %>
|
|
12
|
+
<%= form.password_field :password, autocomplete: "current-password" %>
|
|
13
|
+
<%= form.submit "Sign in", class: "btn" %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% else %>
|
|
16
|
+
<%= button_to "Sign in", "/auth/#{Upright.configuration.auth_provider}", method: :post, data: { turbo: false }, class: "btn" %>
|
|
17
|
+
<% end %>
|
|
5
18
|
</div>
|
|
6
19
|
</div>
|
data/config/ci.rb
CHANGED
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
CI.run do
|
|
4
4
|
step "Style: Ruby", "bin/rubocop"
|
|
5
5
|
step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
|
|
6
|
+
step "Lint: GitHub Actions (actionlint)", "actionlint"
|
|
7
|
+
step "Lint: GitHub Actions (zizmor)", "zizmor ."
|
|
6
8
|
step "Tests: Rails", "bin/rails test"
|
|
7
9
|
end
|
data/config/importmap.rb
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# Two entry points share this map. The admin app loads "application"; the public
|
|
2
|
+
# status page loads "upright/public". Pins used only by the admin app are
|
|
3
|
+
# preloaded for the "application" entry point alone, so the public page does not
|
|
4
|
+
# fetch Turbo, Stimulus, Leaflet or the charts library.
|
|
5
|
+
|
|
6
|
+
# Admin app
|
|
7
|
+
pin "application", to: "upright/application.js", preload: "application"
|
|
8
|
+
pin "upright/application", to: "upright/application.js", preload: "application"
|
|
9
|
+
pin "upright/controllers/application", to: "upright/controllers/application.js", preload: "application"
|
|
10
|
+
pin "upright/controllers", to: "upright/controllers/index.js", preload: "application"
|
|
6
11
|
|
|
7
12
|
pin_all_from Upright::Engine.root.join("app/javascript/upright/controllers"),
|
|
8
13
|
under: "upright/controllers",
|
|
9
|
-
to: "upright/controllers"
|
|
14
|
+
to: "upright/controllers",
|
|
15
|
+
preload: "application"
|
|
16
|
+
|
|
17
|
+
pin "@hotwired/turbo-rails", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo-rails@8.0.20/+esm", preload: "application"
|
|
18
|
+
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.20/+esm", preload: "application"
|
|
19
|
+
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js", preload: "application"
|
|
20
|
+
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: "application"
|
|
21
|
+
pin "@rails/actioncable/src", to: "https://cdn.jsdelivr.net/npm/@rails/actioncable@8.1.100/src/index.js", preload: "application"
|
|
22
|
+
pin "leaflet", to: "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js", preload: "application"
|
|
23
|
+
pin "frappe-charts", to: "https://cdn.jsdelivr.net/npm/frappe-charts@1.6.2/dist/frappe-charts.min.esm.js", preload: "application"
|
|
24
|
+
|
|
25
|
+
# Public status page
|
|
26
|
+
pin "upright/public", to: "upright/public.js", preload: "upright/public"
|
|
10
27
|
|
|
11
|
-
#
|
|
12
|
-
pin "
|
|
13
|
-
pin "@hotwired/turbo", to: "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.20/+esm"
|
|
14
|
-
pin "@hotwired/stimulus", to: "https://cdn.jsdelivr.net/npm/@hotwired/stimulus@3.2.2/dist/stimulus.js"
|
|
15
|
-
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
|
16
|
-
pin "@rails/actioncable/src", to: "https://cdn.jsdelivr.net/npm/@rails/actioncable@8.1.100/src/index.js"
|
|
17
|
-
pin "leaflet", to: "https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet-src.esm.js"
|
|
18
|
-
pin "frappe-charts", to: "https://cdn.jsdelivr.net/npm/frappe-charts@1.6.2/dist/frappe-charts.min.esm.js"
|
|
28
|
+
# Shared
|
|
29
|
+
pin "local-time", to: "local-time.es2017-esm.js", preload: true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Mime::Type.register "application/rss+xml", :rss unless Mime::Type.lookup_by_extension(:rss)
|
data/config/routes.rb
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
Upright::Engine.routes.draw do
|
|
2
|
-
global_subdomain
|
|
3
|
-
site_subdomain
|
|
2
|
+
global_subdomain = ->(req) { Upright.configuration.global_subdomain == req.subdomain }
|
|
3
|
+
site_subdomain = ->(req) { Upright.configuration.site_subdomains.include?(req.subdomain) }
|
|
4
|
+
metrics_subdomain = ->(req) { global_subdomain.call(req) || Upright.find_site(req.subdomain)&.stores_metrics? }
|
|
5
|
+
public_status = ->(req) {
|
|
6
|
+
Upright.configuration.public_status_enabled &&
|
|
7
|
+
req.subdomain == Upright.configuration.public_status_subdomain
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
constraints global_subdomain do
|
|
6
11
|
root "sites#index", as: :admin_root
|
|
@@ -13,6 +18,12 @@ Upright::Engine.routes.draw do
|
|
|
13
18
|
resource :probe_status, only: :show
|
|
14
19
|
end
|
|
15
20
|
|
|
21
|
+
resources :incidents do
|
|
22
|
+
resources :updates, only: :create, controller: "incidents/updates"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
constraints metrics_subdomain do
|
|
16
27
|
scope :framed do
|
|
17
28
|
resource :prometheus, only: :show, controller: :prometheus_proxy
|
|
18
29
|
resource :alertmanager, only: :show, controller: :alertmanager_proxy
|
|
@@ -29,6 +40,8 @@ Upright::Engine.routes.draw do
|
|
|
29
40
|
|
|
30
41
|
resources :artifacts, only: :show, as: :site_artifacts
|
|
31
42
|
|
|
43
|
+
match "traces/:signed_id", to: "traces#show", as: :site_trace, via: [ :get, :options ]
|
|
44
|
+
|
|
32
45
|
scope :framed do
|
|
33
46
|
resource :jobs, only: :show
|
|
34
47
|
end
|
|
@@ -36,6 +49,17 @@ Upright::Engine.routes.draw do
|
|
|
36
49
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
|
37
50
|
end
|
|
38
51
|
|
|
52
|
+
constraints public_status do
|
|
53
|
+
scope module: :public, as: :public do
|
|
54
|
+
root "services#index", as: :services_root
|
|
55
|
+
get "feed", to: "services#index", as: :services_feed, defaults: { format: :rss }
|
|
56
|
+
resources :services, only: [], param: :code do
|
|
57
|
+
resources :incidents, only: :index, controller: :service_incidents
|
|
58
|
+
end
|
|
59
|
+
resources :incidents, only: :show
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
39
63
|
# Base routes (no subdomain constraint)
|
|
40
64
|
resource :session, only: :destroy
|
|
41
65
|
root "sites#index"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class FixProbeResultDurationPrecision < ActiveRecord::Migration[8.0]
|
|
2
|
+
# `t.decimal :duration` with no precision becomes DECIMAL(10,0) on MySQL,
|
|
3
|
+
# which rounds every duration to a whole second on write. Probe durations
|
|
4
|
+
# are sub-second floats from a monotonic clock, so they were all stored as
|
|
5
|
+
# 0. SQLite preserved the fractional value, which is why this only surfaced
|
|
6
|
+
# after the MySQL migration. Give the column microsecond precision.
|
|
7
|
+
def up
|
|
8
|
+
change_column :upright_probe_results, :duration, :decimal, precision: 10, scale: 6
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def down
|
|
12
|
+
change_column :upright_probe_results, :duration, :decimal
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateUprightRollups < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :upright_rollups_probe_rollups do |t|
|
|
4
|
+
t.string :probe_name, null: false
|
|
5
|
+
t.string :probe_service
|
|
6
|
+
t.datetime :period_start, null: false
|
|
7
|
+
t.float :uptime_fraction, null: false
|
|
8
|
+
t.integer :status, default: 0, null: false
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :upright_rollups_probe_rollups, [ :probe_name, :period_start ], unique: true
|
|
14
|
+
add_index :upright_rollups_probe_rollups, [ :probe_service, :period_start ]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class CreateUprightIncidents < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :upright_incidents do |t|
|
|
4
|
+
t.string :type
|
|
5
|
+
t.string :title, null: false
|
|
6
|
+
t.string :status, null: false
|
|
7
|
+
t.string :impact, null: false
|
|
8
|
+
t.datetime :starts_at, null: false
|
|
9
|
+
t.datetime :ends_at
|
|
10
|
+
t.datetime :resolved_at
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_index :upright_incidents, [ :resolved_at, :starts_at ]
|
|
16
|
+
add_index :upright_incidents, [ :type, :starts_at ]
|
|
17
|
+
|
|
18
|
+
create_table :upright_incident_updates do |t|
|
|
19
|
+
t.references :incident, null: false, foreign_key: { to_table: :upright_incidents }
|
|
20
|
+
t.string :status, null: false
|
|
21
|
+
t.text :body
|
|
22
|
+
|
|
23
|
+
t.datetime :created_at, null: false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
create_table :upright_incident_affected_services do |t|
|
|
27
|
+
t.references :incident, null: false, foreign_key: { to_table: :upright_incidents }
|
|
28
|
+
t.string :service_code, null: false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
add_index :upright_incident_affected_services, [ :incident_id, :service_code ], unique: true
|
|
32
|
+
add_index :upright_incident_affected_services, :service_code
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class AddProbeIdentityToUprightRollups < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :upright_rollups_probe_rollups, :probe_type, :string
|
|
4
|
+
add_column :upright_rollups_probe_rollups, :probe_target, :string
|
|
5
|
+
|
|
6
|
+
remove_index :upright_rollups_probe_rollups, column: [ :probe_name, :period_start ], unique: true
|
|
7
|
+
add_index :upright_rollups_probe_rollups,
|
|
8
|
+
[ :probe_name, :probe_type, :probe_target, :period_start ],
|
|
9
|
+
unique: true, name: "idx_probe_rollups_identity_period"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class AddAutoReportingToUprightIncidents < ActiveRecord::Migration[8.0]
|
|
2
|
+
def up
|
|
3
|
+
add_column :upright_incidents, :auto_created, :boolean, default: false, null: false
|
|
4
|
+
add_column :upright_incidents, :last_seen_down_at, :datetime
|
|
5
|
+
add_column :upright_incidents, :recovery_started_at, :datetime
|
|
6
|
+
|
|
7
|
+
create_table :upright_incident_automatic_reports do |t|
|
|
8
|
+
t.references :incident, null: false, foreign_key: { to_table: :upright_incidents }, index: { unique: true }
|
|
9
|
+
t.string :service_code, null: false, index: { unique: true }
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def down
|
|
15
|
+
drop_table :upright_incident_automatic_reports, if_exists: true
|
|
16
|
+
remove_index :upright_incidents, :auto_service_code, if_exists: true
|
|
17
|
+
remove_column :upright_incidents, :auto_service_code, if_exists: true
|
|
18
|
+
remove_column :upright_incidents, :recovery_started_at, if_exists: true
|
|
19
|
+
remove_column :upright_incidents, :last_seen_down_at
|
|
20
|
+
remove_column :upright_incidents, :auto_created
|
|
21
|
+
end
|
|
22
|
+
end
|