upright 0.3.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 +66 -7
- 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/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/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 +36 -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 +1 -0
- 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 -2
- 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 +26 -2
- data/app/models/concerns/upright/probeable.rb +9 -1
- 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 +12 -3
- 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/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 +3 -11
- data/app/models/upright/probes/uptime.rb +2 -9
- 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/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/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/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 +18 -10
- 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/omniauth.rb +7 -3
- data/lib/generators/upright/install/templates/otel_collector.yml +6 -0
- data/lib/generators/upright/install/templates/recurring.yml +20 -0
- data/lib/generators/upright/install/templates/sites.yml +4 -0
- data/lib/generators/upright/install/templates/upright.rb +21 -2
- data/lib/generators/upright/install/templates/upright.rules.yml +11 -4
- data/lib/upright/configuration.rb +113 -6
- data/lib/upright/engine.rb +25 -2
- data/lib/upright/metrics.rb +25 -0
- data/lib/upright/site.rb +31 -1
- data/lib/upright/version.rb +3 -1
- data/lib/upright.rb +29 -2
- metadata +107 -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,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/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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Upright
|
|
2
|
+
module Generators
|
|
3
|
+
# Rewrites the single-database development and test entries that `rails new`
|
|
4
|
+
# writes into the three databases Upright uses:
|
|
5
|
+
#
|
|
6
|
+
# primary probe results and Active Storage artifacts
|
|
7
|
+
# persistent rollups, incidents and maintenance windows, which outlive a
|
|
8
|
+
# site's probe data; its migrations ship with the gem
|
|
9
|
+
# queue Solid Queue
|
|
10
|
+
#
|
|
11
|
+
# Pure string transformation so the install generator's edit can be tested
|
|
12
|
+
# without a generated application.
|
|
13
|
+
module DatabaseConfig
|
|
14
|
+
ENVIRONMENTS = %w[development test].freeze
|
|
15
|
+
|
|
16
|
+
PERSISTENT_MIGRATIONS = %(<%= Gem.loaded_specs["upright"].gem_dir %>/db/persistent_migrate)
|
|
17
|
+
|
|
18
|
+
def self.rewrite(yaml)
|
|
19
|
+
ENVIRONMENTS.reduce(yaml) do |rewritten, env|
|
|
20
|
+
rewritten.sub(single_database(env), split_databases(env))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.single_database(env)
|
|
25
|
+
"#{env}:\n <<: *default\n database: storage/#{env}.sqlite3"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.split_databases(env)
|
|
29
|
+
<<~YAML.chomp
|
|
30
|
+
#{env}:
|
|
31
|
+
primary:
|
|
32
|
+
<<: *default
|
|
33
|
+
database: storage/#{env}.sqlite3
|
|
34
|
+
persistent:
|
|
35
|
+
<<: *default
|
|
36
|
+
database: storage/#{env}_persistent.sqlite3
|
|
37
|
+
migrations_paths: #{PERSISTENT_MIGRATIONS}
|
|
38
|
+
queue:
|
|
39
|
+
<<: *default
|
|
40
|
+
database: storage/#{env}_queue.sqlite3
|
|
41
|
+
migrations_paths: db/queue_migrate
|
|
42
|
+
YAML
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "database_config"
|
|
2
|
+
|
|
1
3
|
module Upright
|
|
2
4
|
module Generators
|
|
3
5
|
class InstallGenerator < Rails::Generators::Base
|
|
@@ -17,6 +19,11 @@ module Upright
|
|
|
17
19
|
def copy_initializers
|
|
18
20
|
template "upright.rb", "config/initializers/upright.rb"
|
|
19
21
|
template "omniauth.rb", "config/initializers/omniauth.rb"
|
|
22
|
+
# Never force: an app may already run an enforced CSP, and silently
|
|
23
|
+
# replacing it with this commented template would disable it. On a
|
|
24
|
+
# collision Thor prompts instead of overwriting; the recommended policy is
|
|
25
|
+
# documented for the operator to merge.
|
|
26
|
+
template "content_security_policy.rb", "config/initializers/content_security_policy.rb"
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
def copy_sites_config
|
|
@@ -46,6 +53,7 @@ module Upright
|
|
|
46
53
|
|
|
47
54
|
def copy_deploy_config
|
|
48
55
|
template "deploy.yml", "config/deploy.yml"
|
|
56
|
+
template "Dockerfile", "Dockerfile"
|
|
49
57
|
end
|
|
50
58
|
|
|
51
59
|
def copy_puma_config
|
|
@@ -56,14 +64,14 @@ module Upright
|
|
|
56
64
|
rails_command "solid_queue:install"
|
|
57
65
|
end
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
gsub_file "config/database.yml",
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
# Splits the development and test databases into primary, persistent and
|
|
68
|
+
# queue. The persistent database holds rollups, incidents and maintenance
|
|
69
|
+
# windows, and its migrations come from the gem, so database.yml points
|
|
70
|
+
# migrations_paths at the installed gem's db/persistent_migrate.
|
|
71
|
+
def add_persistent_and_queue_databases
|
|
72
|
+
gsub_file "config/database.yml", /\A.*\z/m do |yaml|
|
|
73
|
+
DatabaseConfig.rewrite(yaml)
|
|
74
|
+
end
|
|
67
75
|
end
|
|
68
76
|
|
|
69
77
|
def copy_recurring_config
|
|
@@ -101,11 +109,11 @@ module Upright
|
|
|
101
109
|
say "Upright has been installed!", :green
|
|
102
110
|
say ""
|
|
103
111
|
say "Next steps:"
|
|
104
|
-
say " 1. Prepare the
|
|
112
|
+
say " 1. Prepare the databases (primary, persistent and queue): bin/rails db:prepare"
|
|
105
113
|
say " 2. Configure your servers in config/deploy.yml"
|
|
106
114
|
say " 3. Configure sites in config/sites.yml"
|
|
107
115
|
say " 4. Add probes in probes/*.yml"
|
|
108
|
-
say " 5. Set ADMIN_PASSWORD env var
|
|
116
|
+
say " 5. Set the ADMIN_PASSWORD env var — required, there is no default password"
|
|
109
117
|
say ""
|
|
110
118
|
say "For production, review config/initializers/upright.rb and update:"
|
|
111
119
|
say " config.hostname = \"example.com\""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# syntax = docker/dockerfile:1
|
|
2
|
+
|
|
3
|
+
# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
|
|
4
|
+
# docker build -t my-app .
|
|
5
|
+
# docker run -d -p 80:80 -p 443:443 --name my-app -e RAILS_MASTER_KEY=<value from config/master.key> my-app
|
|
6
|
+
|
|
7
|
+
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
|
|
8
|
+
ARG RUBY_VERSION=<%= RUBY_VERSION %>
|
|
9
|
+
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
|
|
10
|
+
|
|
11
|
+
# Rails app lives here
|
|
12
|
+
WORKDIR /rails
|
|
13
|
+
|
|
14
|
+
# Install base packages and Node.js (for Playwright)
|
|
15
|
+
RUN apt-get update -qq && \
|
|
16
|
+
apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 iputils-ping mtr-tiny \
|
|
17
|
+
nodejs npm && \
|
|
18
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
19
|
+
|
|
20
|
+
# Set production environment
|
|
21
|
+
ENV RAILS_ENV="production" \
|
|
22
|
+
BUNDLE_DEPLOYMENT="1" \
|
|
23
|
+
BUNDLE_PATH="/usr/local/bundle" \
|
|
24
|
+
BUNDLE_WITHOUT="development"
|
|
25
|
+
|
|
26
|
+
# Throw-away build stage to reduce size of final image
|
|
27
|
+
FROM base AS build
|
|
28
|
+
|
|
29
|
+
# Install packages needed to build gems
|
|
30
|
+
RUN apt-get update -qq && \
|
|
31
|
+
apt-get install --no-install-recommends -y build-essential git pkg-config libssl-dev libyaml-dev rustc cargo libclang-dev && \
|
|
32
|
+
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
|
33
|
+
|
|
34
|
+
# Install application gems
|
|
35
|
+
COPY Gemfile Gemfile.lock ./
|
|
36
|
+
RUN --mount=type=secret,id=GITHUB_TOKEN \
|
|
37
|
+
BUNDLE_GITHUB__COM="$(cat /run/secrets/GITHUB_TOKEN):x-oauth-basic" bundle install && \
|
|
38
|
+
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
|
|
39
|
+
bundle exec bootsnap precompile --gemfile && \
|
|
40
|
+
bundle exec ruby -e "require 'upright/version'; puts Upright::PLAYWRIGHT_VERSION" > /tmp/playwright_version
|
|
41
|
+
|
|
42
|
+
# Copy application code
|
|
43
|
+
COPY . .
|
|
44
|
+
|
|
45
|
+
# Precompile bootsnap code for faster boot times
|
|
46
|
+
RUN bundle exec bootsnap precompile app.rb probes/
|
|
47
|
+
|
|
48
|
+
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
|
|
49
|
+
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
|
|
50
|
+
|
|
51
|
+
# Final stage for app image
|
|
52
|
+
FROM base
|
|
53
|
+
|
|
54
|
+
# Install Playwright before copying app code so the layer is cached on code-only deploys.
|
|
55
|
+
# The version is extracted to a file in the build stage; BuildKit's content-based caching
|
|
56
|
+
# keeps this layer cached even across Gemfile changes when the version is unchanged.
|
|
57
|
+
COPY --from=build /tmp/playwright_version /tmp/playwright_version
|
|
58
|
+
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
|
59
|
+
RUN PLAYWRIGHT_VERSION=$(cat /tmp/playwright_version) && \
|
|
60
|
+
npm install -g playwright@~${PLAYWRIGHT_VERSION}.0 && \
|
|
61
|
+
playwright install --with-deps chromium && \
|
|
62
|
+
chmod -R o+rx /ms-playwright
|
|
63
|
+
|
|
64
|
+
# Copy built artifacts: gems, application
|
|
65
|
+
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
|
|
66
|
+
COPY --from=build /rails /rails
|
|
67
|
+
|
|
68
|
+
# Run and own only the runtime files as a non-root user for security
|
|
69
|
+
RUN groupadd --system --gid 1000 rails && \
|
|
70
|
+
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
|
|
71
|
+
mkdir -p storage tmp && \
|
|
72
|
+
chown -R rails:rails storage tmp
|
|
73
|
+
USER 1000:1000
|
|
74
|
+
|
|
75
|
+
# Entrypoint prepares the database.
|
|
76
|
+
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
|
|
77
|
+
|
|
78
|
+
# Start the server by default, this can be overwritten at runtime
|
|
79
|
+
EXPOSE 3000 9394
|
|
80
|
+
CMD ["./bin/rails", "server"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
|
2
|
+
|
|
3
|
+
# Content Security Policy tuned for Upright. Uncomment the block below to
|
|
4
|
+
# enforce it. Every source the engine actually uses is listed explicitly:
|
|
5
|
+
#
|
|
6
|
+
# * JavaScript dependencies (Turbo, Stimulus, Leaflet, Frappe Charts) are
|
|
7
|
+
# pinned to https://cdn.jsdelivr.net in the engine's config/importmap.rb.
|
|
8
|
+
# * The Leaflet stylesheet is loaded from https://unpkg.com in the engine
|
|
9
|
+
# layout; Turbo injects an inline <style> for its progress bar, which is
|
|
10
|
+
# why style-src keeps :unsafe_inline (styles only — scripts stay strict).
|
|
11
|
+
# * Map tiles come from OpenStreetMap (light) and Carto (dark); Leaflet's
|
|
12
|
+
# default marker icons load from unpkg alongside its stylesheet.
|
|
13
|
+
# * The inline <script type="importmap"> tag is authorized by a per-session
|
|
14
|
+
# nonce via csp_meta_tag, which the engine layouts already render.
|
|
15
|
+
#
|
|
16
|
+
# To verify without breaking anything, start with
|
|
17
|
+
# config.content_security_policy_report_only = true and watch the browser
|
|
18
|
+
# console before enforcing.
|
|
19
|
+
|
|
20
|
+
# Rails.application.configure do
|
|
21
|
+
# config.content_security_policy do |policy|
|
|
22
|
+
# policy.default_src :self
|
|
23
|
+
# policy.font_src :self, :data
|
|
24
|
+
# policy.img_src :self, :data, "https://unpkg.com",
|
|
25
|
+
# "https://*.tile.openstreetmap.org"
|
|
26
|
+
# policy.object_src :none
|
|
27
|
+
# policy.script_src :self, "https://cdn.jsdelivr.net"
|
|
28
|
+
# policy.style_src :self, :unsafe_inline, "https://unpkg.com"
|
|
29
|
+
# policy.connect_src :self
|
|
30
|
+
# policy.frame_ancestors :self
|
|
31
|
+
# policy.base_uri :self
|
|
32
|
+
# # Specify URI for violation reports
|
|
33
|
+
# # policy.report_uri "/csp-violation-report-endpoint"
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# # Generate session nonces for the inline importmap script tag.
|
|
37
|
+
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
|
|
38
|
+
# config.content_security_policy_nonce_directives = %w(script-src)
|
|
39
|
+
#
|
|
40
|
+
# # Report violations without enforcing the policy.
|
|
41
|
+
# # config.content_security_policy_report_only = true
|
|
42
|
+
# end
|