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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -7
  3. data/Rakefile +22 -0
  4. data/app/assets/images/upright/.keep +0 -0
  5. data/app/assets/stylesheets/upright/_global.css +2 -0
  6. data/app/assets/stylesheets/upright/artifact.css +7 -25
  7. data/app/assets/stylesheets/upright/flash.css +19 -0
  8. data/app/assets/stylesheets/upright/header.css +14 -0
  9. data/app/assets/stylesheets/upright/incidents.css +610 -0
  10. data/app/assets/stylesheets/upright/map.css +6 -0
  11. data/app/assets/stylesheets/upright/public_status.css +534 -0
  12. data/app/controllers/concerns/.keep +0 -0
  13. data/app/controllers/concerns/upright/authentication.rb +1 -0
  14. data/app/controllers/concerns/upright/proxy_authentication.rb +134 -0
  15. data/app/controllers/upright/alertmanager_proxy_controller.rb +12 -4
  16. data/app/controllers/upright/artifacts_controller.rb +1 -1
  17. data/app/controllers/upright/incidents/updates_controller.rb +13 -0
  18. data/app/controllers/upright/incidents_controller.rb +53 -0
  19. data/app/controllers/upright/prometheus_proxy_controller.rb +13 -18
  20. data/app/controllers/upright/public/base_controller.rb +16 -0
  21. data/app/controllers/upright/public/incidents_controller.rb +6 -0
  22. data/app/controllers/upright/public/service_incidents_controller.rb +7 -0
  23. data/app/controllers/upright/public/services_controller.rb +6 -0
  24. data/app/controllers/upright/sessions_controller.rb +36 -1
  25. data/app/controllers/upright/traces_controller.rb +88 -0
  26. data/app/helpers/upright/application_helper.rb +22 -2
  27. data/app/helpers/upright/artifacts_helper.rb +20 -0
  28. data/app/helpers/upright/probe_results_helper.rb +1 -0
  29. data/app/helpers/upright/public/services_helper.rb +70 -0
  30. data/app/javascript/upright/application.js +3 -0
  31. data/app/javascript/upright/controllers/sites_map_controller.js +13 -5
  32. data/app/javascript/upright/controllers/template_controller.js +11 -0
  33. data/app/javascript/upright/public.js +5 -0
  34. data/app/jobs/upright/health_metrics_job.rb +27 -0
  35. data/app/jobs/upright/incident_reporter_job.rb +7 -0
  36. data/app/jobs/upright/maintenance_advance_job.rb +8 -0
  37. data/app/jobs/upright/playwright_video_sweep_job.rb +21 -0
  38. data/app/jobs/upright/rollups/daily_aggregation_job.rb +14 -0
  39. data/app/models/concerns/.keep +0 -0
  40. data/app/models/concerns/upright/incidents/auto_reporting.rb +93 -0
  41. data/app/models/concerns/upright/incidents/lifecycle.rb +47 -0
  42. data/app/models/concerns/upright/playwright/form_authentication.rb +3 -2
  43. data/app/models/concerns/upright/playwright/lifecycle.rb +8 -13
  44. data/app/models/concerns/upright/playwright/trace_recording.rb +53 -0
  45. data/app/models/concerns/upright/playwright/video_recording.rb +55 -14
  46. data/app/models/concerns/upright/probe_result/stale_cleanup.rb +26 -2
  47. data/app/models/concerns/upright/probeable.rb +9 -1
  48. data/app/models/concerns/upright/services/live_status.rb +69 -0
  49. data/app/models/concerns/upright/services/maintenance_status.rb +19 -0
  50. data/app/models/upright/artifact.rb +2 -1
  51. data/app/models/upright/http/request.rb +12 -3
  52. data/app/models/upright/incident/automatic_report.rb +3 -0
  53. data/app/models/upright/incident.rb +87 -0
  54. data/app/models/upright/incident_affected_service.rb +9 -0
  55. data/app/models/upright/incident_update.rb +7 -0
  56. data/app/models/upright/maintenance.rb +36 -0
  57. data/app/models/upright/persistent_record.rb +12 -0
  58. data/app/models/upright/playwright/authenticator/base.rb +50 -70
  59. data/app/models/upright/probes/http_probe.rb +30 -1
  60. data/app/models/upright/probes/playwright/base.rb +2 -0
  61. data/app/models/upright/probes/status/probe.rb +8 -0
  62. data/app/models/upright/probes/status.rb +3 -11
  63. data/app/models/upright/probes/uptime.rb +2 -9
  64. data/app/models/upright/rollups/daily_uptime.rb +28 -0
  65. data/app/models/upright/rollups/probe_rollup.rb +64 -0
  66. data/app/models/upright/rollups/probe_uptime.rb +9 -0
  67. data/app/models/upright/rollups/site_uptime.rb +75 -0
  68. data/app/models/upright/service/daily_status.rb +39 -0
  69. data/app/models/upright/service/incident_history.rb +24 -0
  70. data/app/models/upright/service/outage.rb +15 -0
  71. data/app/models/upright/service/status_page.rb +35 -0
  72. data/app/models/upright/service.rb +101 -0
  73. data/app/models/upright/status.rb +21 -0
  74. data/app/models/upright/traceroute/ip_metadata_lookup.rb +21 -2
  75. data/app/models/upright/traceroute/result.rb +8 -1
  76. data/app/views/layouts/upright/_header.html.erb +12 -2
  77. data/app/views/layouts/upright/application.html.erb +4 -1
  78. data/app/views/layouts/upright/public.html.erb +18 -0
  79. data/app/views/layouts/upright/public.rss.builder +1 -0
  80. data/app/views/upright/active_storage/attachments/_attachment.html.erb +1 -1
  81. data/app/views/upright/artifacts/show.html.erb +8 -0
  82. data/app/views/upright/incidents/_form.html.erb +93 -0
  83. data/app/views/upright/incidents/_row.html.erb +15 -0
  84. data/app/views/upright/incidents/edit.html.erb +64 -0
  85. data/app/views/upright/incidents/index.html.erb +53 -0
  86. data/app/views/upright/incidents/new.html.erb +6 -0
  87. data/app/views/upright/public/_branding.html.erb +3 -0
  88. data/app/views/upright/public/_footer.html.erb +3 -0
  89. data/app/views/upright/public/incidents/show.html.erb +29 -0
  90. data/app/views/upright/public/service_incidents/_incident.html.erb +14 -0
  91. data/app/views/upright/public/service_incidents/index.html.erb +50 -0
  92. data/app/views/upright/public/services/_active_events.html.erb +28 -0
  93. data/app/views/upright/public/services/_degraded_list.html.erb +14 -0
  94. data/app/views/upright/public/services/_overall_banner.html.erb +4 -0
  95. data/app/views/upright/public/services/_service.html.erb +34 -0
  96. data/app/views/upright/public/services/_upcoming_maintenance.html.erb +21 -0
  97. data/app/views/upright/public/services/_uptime_bar.html.erb +9 -0
  98. data/app/views/upright/public/services/index.html.erb +13 -0
  99. data/app/views/upright/public/services/index.rss.builder +29 -0
  100. data/app/views/upright/sessions/new.html.erb +14 -1
  101. data/config/importmap.rb +25 -14
  102. data/config/initializers/date_formats.rb +5 -0
  103. data/config/initializers/mime_types.rb +1 -0
  104. data/config/routes.rb +26 -2
  105. data/db/migrate/20260629000001_fix_probe_result_duration_precision.rb +14 -0
  106. data/db/persistent_migrate/20260512000001_create_upright_rollups.rb +16 -0
  107. data/db/persistent_migrate/20260702000001_create_upright_incidents.rb +34 -0
  108. data/db/persistent_migrate/20260707000001_add_probe_identity_to_upright_rollups.rb +11 -0
  109. data/db/persistent_migrate/20260709000001_add_created_by_to_incidents.rb +7 -0
  110. data/db/persistent_migrate/20260901000001_add_auto_reporting_to_upright_incidents.rb +22 -0
  111. data/lib/generators/upright/install/database_config.rb +46 -0
  112. data/lib/generators/upright/install/install_generator.rb +18 -10
  113. data/lib/generators/upright/install/templates/Dockerfile +80 -0
  114. data/lib/generators/upright/install/templates/content_security_policy.rb +42 -0
  115. data/lib/generators/upright/install/templates/deploy.yml +13 -13
  116. data/lib/generators/upright/install/templates/development_prometheus.yml +1 -1
  117. data/lib/generators/upright/install/templates/docker-compose.yml +12 -11
  118. data/lib/generators/upright/install/templates/omniauth.rb +7 -3
  119. data/lib/generators/upright/install/templates/otel_collector.yml +6 -0
  120. data/lib/generators/upright/install/templates/recurring.yml +20 -0
  121. data/lib/generators/upright/install/templates/sites.yml +4 -0
  122. data/lib/generators/upright/install/templates/upright.rb +21 -2
  123. data/lib/generators/upright/install/templates/upright.rules.yml +11 -4
  124. data/lib/upright/configuration.rb +113 -6
  125. data/lib/upright/engine.rb +25 -2
  126. data/lib/upright/metrics.rb +25 -0
  127. data/lib/upright/site.rb +31 -1
  128. data/lib/upright/version.rb +3 -1
  129. data/lib/upright.rb +29 -2
  130. metadata +107 -7
  131. data/config/credentials/development.key +0 -1
  132. data/config/credentials/test.key +0 -1
  133. data/lib/tasks/upright_tasks.rake +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccb561e794bdafa5e31caed2347f7d371d02e5230ca875c5f507d1d0620d7363
4
- data.tar.gz: 3bc20100e850cdd93a81e58b7e9b6f3ef9377c44b17bbd7891dbe697699b23a6
3
+ metadata.gz: c91a962bc77007bffeebba4df5da09c213a42c46a416de5fce7d19e9169258f7
4
+ data.tar.gz: 47b63615e16a15aede26f738a3ffc7256fada1aab09b775ff7ddd90282c91ea0
5
5
  SHA512:
6
- metadata.gz: 474970b3ccbce05a396ceb2a3575a58272b602d679cf70d55d10538cc546aa31d6142e1de401ee5098c6c3a2733a624cba54e34b79a0dcd9c6af7ad7e8124f0d
7
- data.tar.gz: 35ed0971379b1c50dd9583f95ed27aa2586f53002e8acd81c05851797d6f2d1c59ced8d6a08817613ecb89ab376d453a9697cf241b82be1409c11f1caa87c6d3
6
+ metadata.gz: 25a77e92224a4869f1da746deba9f5a4440c4a071d2e3b0c8fef363c2abd36e270cc9a1147a11081284a85f0deb8e22cc73a09d8a3dcd183ea29dd7a0419e872
7
+ data.tar.gz: a5e047ccb8feb9e864652496e81cdbb17ea4e0ae0972b8e380df9fa1206b011c23d5fd0b1e8bae2f366313dcb48b7af25d3babeba1f96e1211967bf70e954415
data/README.md CHANGED
@@ -59,7 +59,7 @@ rails new my-upright --database=sqlite3 --skip-test
59
59
  cd my-upright
60
60
  bundle add upright
61
61
  bin/rails generate upright:install
62
- bin/rails db:migrate
62
+ bin/rails db:prepare
63
63
  ```
64
64
 
65
65
  Start the server:
@@ -76,7 +76,10 @@ Visit http://app.my-upright.localhost:3000 to see your Upright instance.
76
76
 
77
77
  The `upright:install` generator creates:
78
78
 
79
+ - `config/database.yml` - Development and test split into `primary` (probe results), `persistent` (rollups and incidents, migrated from the gem) and `queue` (Solid Queue) databases
80
+ - `config/recurring.yml` - Probe schedules and the engine's housekeeping, health, rollup, incident and maintenance jobs
79
81
  - `config/initializers/upright.rb` - Engine configuration
82
+ - `config/initializers/content_security_policy.rb` - Ready-to-enable CSP covering the engine's CDN dependencies
80
83
  - `config/sites.yml` - Site definitions for each VPS you host Upright on
81
84
  - `config/prometheus/prometheus.yml` - Prometheus configuration
82
85
  - `config/alertmanager/alertmanager.yml` - AlertManager configuration
@@ -122,6 +125,8 @@ shared:
122
125
  country: NL
123
126
  geohash: u17982
124
127
  provider: digitalocean
128
+ stores_metrics: true
129
+ primary: true
125
130
 
126
131
  - code: sfo
127
132
  city: San Francisco
@@ -132,14 +137,16 @@ shared:
132
137
 
133
138
  Each site node identifies itself via the `SITE_SUBDOMAIN` environment variable, configured in your Kamal deploy.yml.
134
139
 
140
+ Two optional flags give a site a role beyond running probes: `primary` serves the app and status hostnames and runs the jobs writing the shared database, and `stores_metrics` runs a local Prometheus and Alertmanager. See [Sites and their roles](https://github.com/basecamp/upright/blob/main/docs/sites.md) for what each one changes, the health metrics every site exports, and how daily rollups read across them.
141
+
135
142
  ### Authentication
136
143
 
137
144
  #### Static Credentials
138
145
 
139
- Upright uses static credentials by default with username `admin` and password `upright`.
146
+ Upright uses static credentials by default with username `admin` and the password taken from the `ADMIN_PASSWORD` environment variable.
140
147
 
141
- > [!WARNING]
142
- > Change the default password before deploying to production by setting the `ADMIN_PASSWORD` environment variable.
148
+ > [!IMPORTANT]
149
+ > There is no default password: `ADMIN_PASSWORD` must be set, and sign-in fails closed without it. Set it as a Kamal secret in production (the generated `config/deploy.yml` already lists it) and export it locally for development.
143
150
 
144
151
 
145
152
  #### OpenID Connect
@@ -197,8 +204,14 @@ class PingProbe < FrozenRecord::Base
197
204
 
198
205
  stagger_by_site 3.seconds
199
206
 
207
+ # Hostnames, IPv4, and IPv6 addresses only. The first character may not be
208
+ # a dash, so a configured host can never be mistaken for a ping flag.
209
+ HOST_PATTERN = /\A[a-z0-9:][a-z0-9:._-]*\z/i
210
+
200
211
  def check
201
- @ping_output, status = Open3.capture2e("ping", "-c", "1", "-W", "5", host)
212
+ raise ArgumentError, "invalid ping host: #{host.inspect}" unless HOST_PATTERN.match?(host.to_s)
213
+
214
+ @ping_output, status = Open3.capture2e("ping", "-c", "1", "-W", "5", "--", host)
202
215
  status.success?
203
216
  end
204
217
 
@@ -425,6 +438,7 @@ proxy:
425
438
  env:
426
439
  secret:
427
440
  - RAILS_MASTER_KEY
441
+ - ADMIN_PASSWORD
428
442
  tags:
429
443
  amsterdam:
430
444
  SITE_SUBDOMAIN: ams
@@ -536,20 +550,65 @@ bin/dev
536
550
 
537
551
  Visit http://app.upright.localhost:3000 and sign in with:
538
552
  - **Username**: `admin`
539
- - **Password**: `upright` (or value of `ADMIN_PASSWORD` env var)
553
+ - **Password**: the value of the `ADMIN_PASSWORD` env var, e.g. `ADMIN_PASSWORD=upright bin/dev` (a throwaway value like this is fine for local testing only — never for a deployed site)
540
554
 
541
555
  ### Testing Playwright Probes
542
556
 
543
557
  Run probes with a visible browser window:
544
558
 
545
559
  ```bash
546
- LOCAL_PLAYWRIGHT=1 bin/rails console
560
+ HEADLESS=false bin/rails console
547
561
  ```
548
562
 
549
563
  ```ruby
550
564
  Probes::Playwright::MyServiceAuthProbe.check
551
565
  ```
552
566
 
567
+ ### Upgrading Playwright
568
+
569
+ Playwright versions are pinned via `Upright::PLAYWRIGHT_VERSION` in `lib/upright/version.rb`. This drives the Ruby gem and npm package versions. To upgrade:
570
+
571
+ 1. Update `PLAYWRIGHT_VERSION` in `lib/upright/version.rb`
572
+ 2. Update the version in `package.json`
573
+ 3. Run `bin/setup` (or manually: `npm install && npx playwright install chromium`)
574
+ 4. Run `bin/rails test` to verify compatibility
575
+ 5. Commit the updated `package.json` and `package-lock.json`
576
+
577
+ ### Viewing Playwright Traces
578
+
579
+ Upright stores a Playwright trace as a probe result artifact and does not serve
580
+ the Playwright Trace Viewer. The viewer renders a trace's tags and attributes as
581
+ HTML in its own origin, so serving it from Upright's hostname would let a trace
582
+ run script against an admin session.
583
+
584
+ A trace artifact links to `config.trace_viewer_url`, which defaults to
585
+ upstream's hosted viewer at `https://trace.playwright.dev`. That viewer runs
586
+ entirely in the browser, and being on its own registrable domain is what keeps a
587
+ trace's contents off Upright's origin.
588
+
589
+ Following the link hands that origin a URL it can read for 24 hours. Set
590
+ `config.trace_viewer_url` to a viewer you host to keep traces to yourself, or
591
+ assign `nil` to keep them download-only:
592
+
593
+ ```bash
594
+ npx playwright show-trace trace.zip
595
+ ```
596
+
597
+ A URL under `config.hostname` raises `Upright::ConfigurationError`, since that
598
+ would put the trace back on the admin origin.
599
+
600
+ That trace URL is served by `Upright::TracesController`, not Active Storage. The
601
+ viewer fetches it from its own origin, so the request arrives without the admin
602
+ session and needs `Access-Control-Allow-Origin` to be readable at all. The
603
+ controller sends that header for the configured viewer's origin and nothing
604
+ else, and takes a signed id in place of the session: scoped to a purpose,
605
+ expiring after 24 hours, and resolvable only to a blob attached to an
606
+ `Upright::ProbeResult` under a `.zip` filename. With no viewer configured the
607
+ route 404s, so it exists only where it's used.
608
+
609
+ Range requests are answered, which is how the viewer reads a ZIP's central
610
+ directory without downloading the whole archive.
611
+
553
612
  ### Running Tests
554
613
 
555
614
  ```bash
data/Rakefile CHANGED
@@ -4,3 +4,25 @@ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
5
 
6
6
  require "bundler/gem_tasks"
7
+
8
+ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
9
+
10
+ desc "Tag v<current version> and push it, which starts the Release workflow"
11
+ task :tag do
12
+ require_relative "lib/upright/version"
13
+ tag = "v#{Upright::VERSION}"
14
+
15
+ abort "tag: the working tree has uncommitted changes" unless `git status --porcelain`.empty?
16
+
17
+ branch = `git rev-parse --abbrev-ref HEAD`.strip
18
+ abort "tag: run this on main (currently on #{branch})" unless branch == "main"
19
+
20
+ sh "git fetch origin main --quiet"
21
+ abort "tag: HEAD differs from origin/main; pull or push first" unless `git rev-parse HEAD`.strip == `git rev-parse origin/main`.strip
22
+ abort "tag: #{tag} already exists locally" unless `git tag --list #{tag}`.strip.empty?
23
+ abort "tag: #{tag} already exists on origin" unless `git ls-remote --tags origin refs/tags/#{tag}`.strip.empty?
24
+
25
+ sh "git tag --annotate #{tag} --message 'upright #{tag}'"
26
+ sh "git push origin #{tag}"
27
+ puts "Pushed #{tag}. Approve the release-rubygems environment when the Release workflow pauses."
28
+ end
File without changes
@@ -35,6 +35,7 @@
35
35
 
36
36
  --lch-green-dark: 55% 0.162 147;
37
37
  --lch-red-dark: 59% 0.19 38;
38
+ --lch-orange-dark: 70% 0.17 65;
38
39
 
39
40
  /* Colors: Semantic */
40
41
  --color-black: oklch(var(--lch-black));
@@ -52,6 +53,7 @@
52
53
  --color-link: oklch(var(--lch-blue-dark));
53
54
  --color-positive: oklch(var(--lch-green-dark));
54
55
  --color-negative: oklch(var(--lch-red-dark));
56
+ --color-warning: oklch(var(--lch-orange-dark));
55
57
  --color-selected: oklch(var(--lch-blue-lighter));
56
58
 
57
59
  /* Borders & Shadows */
@@ -22,17 +22,11 @@
22
22
 
23
23
  .artifact__content {
24
24
  background: var(--color-ink-lightest);
25
- border-radius: 0.25rem;
26
- box-shadow: var(--shadow);
27
25
  display: flex;
28
26
  flex-direction: column;
29
- height: 80vh;
30
- left: 50%;
27
+ inset: 0;
31
28
  overflow: hidden;
32
29
  position: fixed;
33
- top: 50%;
34
- transform: translate(-50%, -50%);
35
- width: 80vw;
36
30
  z-index: 1000;
37
31
  }
38
32
 
@@ -123,26 +117,14 @@
123
117
  }
124
118
  }
125
119
 
126
- td .artifact + .artifact {
127
- margin-left: calc(var(--inline-space) / 2);
120
+ .artifact__trace {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 0.5em;
128
124
  }
129
125
 
130
- @media (max-width: 140ch) {
131
- .artifact__content {
132
- width: calc(100vw - var(--block-space) * 2);
133
- height: calc(100vh - var(--block-space) * 2);
134
- left: 50%;
135
- top: 50%;
136
- }
126
+ td .artifact + .artifact {
127
+ margin-left: calc(var(--inline-space) / 2);
137
128
  }
138
129
 
139
- @media (max-width: 55ch) {
140
- .artifact__content {
141
- inset: 0;
142
- width: 100%;
143
- height: 100%;
144
- transform: none;
145
- border-radius: 0;
146
- }
147
- }
148
130
  }
@@ -0,0 +1,19 @@
1
+ @layer components {
2
+ .flash {
3
+ border-radius: 0.5rem;
4
+ color: var(--color-ink);
5
+ font-size: var(--text-small);
6
+ margin-block-end: var(--block-space);
7
+ padding: 0.75rem 1rem;
8
+ }
9
+
10
+ .flash--notice {
11
+ background: color-mix(in oklab, var(--color-positive) 12%, var(--color-canvas));
12
+ border: 1px solid color-mix(in oklab, var(--color-positive) 40%, var(--color-canvas));
13
+ }
14
+
15
+ .flash--alert {
16
+ background: color-mix(in oklab, var(--color-negative) 12%, var(--color-canvas));
17
+ border: 1px solid color-mix(in oklab, var(--color-negative) 40%, var(--color-canvas));
18
+ }
19
+ }
@@ -38,6 +38,15 @@
38
38
  text-transform: uppercase;
39
39
  }
40
40
 
41
+ .header__env {
42
+ color: var(--color-warning);
43
+ font-size: var(--text-x-small);
44
+ font-weight: 500;
45
+ letter-spacing: 0.06em;
46
+ margin-right: calc(var(--inline-space) * 2);
47
+ text-transform: uppercase;
48
+ }
49
+
41
50
  .header__nav {
42
51
  align-items: center;
43
52
  display: flex;
@@ -62,6 +71,11 @@
62
71
  &.active {
63
72
  color: var(--color-positive);
64
73
  }
74
+
75
+ &.unavailable {
76
+ color: var(--color-ink-light);
77
+ cursor: help;
78
+ }
65
79
  }
66
80
 
67
81
  .header__divider {