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.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +208 -56
  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/assets/stylesheets/upright/uptime-bars.css +10 -1
  13. data/app/controllers/concerns/.keep +0 -0
  14. data/app/controllers/concerns/upright/authentication.rb +1 -0
  15. data/app/controllers/concerns/upright/proxy_authentication.rb +134 -0
  16. data/app/controllers/upright/alertmanager_proxy_controller.rb +12 -4
  17. data/app/controllers/upright/artifacts_controller.rb +1 -1
  18. data/app/controllers/upright/incidents/updates_controller.rb +13 -0
  19. data/app/controllers/upright/incidents_controller.rb +53 -0
  20. data/app/controllers/upright/probe_results_controller.rb +1 -0
  21. data/app/controllers/upright/prometheus_proxy_controller.rb +13 -18
  22. data/app/controllers/upright/public/base_controller.rb +16 -0
  23. data/app/controllers/upright/public/incidents_controller.rb +6 -0
  24. data/app/controllers/upright/public/service_incidents_controller.rb +7 -0
  25. data/app/controllers/upright/public/services_controller.rb +6 -0
  26. data/app/controllers/upright/sessions_controller.rb +37 -1
  27. data/app/controllers/upright/traces_controller.rb +88 -0
  28. data/app/helpers/upright/application_helper.rb +22 -2
  29. data/app/helpers/upright/artifacts_helper.rb +20 -0
  30. data/app/helpers/upright/probe_results_helper.rb +4 -10
  31. data/app/helpers/upright/public/services_helper.rb +70 -0
  32. data/app/javascript/upright/application.js +3 -0
  33. data/app/javascript/upright/controllers/sites_map_controller.js +13 -5
  34. data/app/javascript/upright/controllers/template_controller.js +11 -0
  35. data/app/javascript/upright/public.js +5 -0
  36. data/app/jobs/upright/health_metrics_job.rb +27 -0
  37. data/app/jobs/upright/incident_reporter_job.rb +7 -0
  38. data/app/jobs/upright/maintenance_advance_job.rb +8 -0
  39. data/app/jobs/upright/playwright_video_sweep_job.rb +21 -0
  40. data/app/jobs/upright/rollups/daily_aggregation_job.rb +14 -0
  41. data/app/models/concerns/.keep +0 -0
  42. data/app/models/concerns/upright/incidents/auto_reporting.rb +93 -0
  43. data/app/models/concerns/upright/incidents/lifecycle.rb +47 -0
  44. data/app/models/concerns/upright/playwright/form_authentication.rb +3 -5
  45. data/app/models/concerns/upright/playwright/lifecycle.rb +8 -13
  46. data/app/models/concerns/upright/playwright/trace_recording.rb +53 -0
  47. data/app/models/concerns/upright/playwright/video_recording.rb +55 -14
  48. data/app/models/concerns/upright/probe_result/stale_cleanup.rb +47 -0
  49. data/app/models/concerns/upright/probeable.rb +16 -2
  50. data/app/models/concerns/upright/services/live_status.rb +69 -0
  51. data/app/models/concerns/upright/services/maintenance_status.rb +19 -0
  52. data/app/models/upright/artifact.rb +2 -1
  53. data/app/models/upright/http/request.rb +13 -4
  54. data/app/models/upright/incident/automatic_report.rb +3 -0
  55. data/app/models/upright/incident.rb +87 -0
  56. data/app/models/upright/incident_affected_service.rb +9 -0
  57. data/app/models/upright/incident_update.rb +7 -0
  58. data/app/models/upright/maintenance.rb +36 -0
  59. data/app/models/upright/persistent_record.rb +12 -0
  60. data/app/models/upright/playwright/authenticator/base.rb +50 -70
  61. data/app/models/upright/probe_result.rb +6 -1
  62. data/app/models/upright/probes/http_probe.rb +30 -1
  63. data/app/models/upright/probes/playwright/base.rb +2 -0
  64. data/app/models/upright/probes/status/probe.rb +8 -0
  65. data/app/models/upright/probes/status.rb +5 -13
  66. data/app/models/upright/probes/uptime.rb +5 -12
  67. data/app/models/upright/rollups/daily_uptime.rb +28 -0
  68. data/app/models/upright/rollups/probe_rollup.rb +64 -0
  69. data/app/models/upright/rollups/probe_uptime.rb +9 -0
  70. data/app/models/upright/rollups/site_uptime.rb +75 -0
  71. data/app/models/upright/service/daily_status.rb +39 -0
  72. data/app/models/upright/service/incident_history.rb +24 -0
  73. data/app/models/upright/service/outage.rb +15 -0
  74. data/app/models/upright/service/status_page.rb +35 -0
  75. data/app/models/upright/service.rb +101 -0
  76. data/app/models/upright/status.rb +21 -0
  77. data/app/models/upright/traceroute/ip_metadata_lookup.rb +21 -2
  78. data/app/models/upright/traceroute/result.rb +8 -1
  79. data/app/views/layouts/upright/_header.html.erb +12 -2
  80. data/app/views/layouts/upright/application.html.erb +4 -1
  81. data/app/views/layouts/upright/public.html.erb +18 -0
  82. data/app/views/layouts/upright/public.rss.builder +1 -0
  83. data/app/views/upright/active_storage/attachments/_attachment.html.erb +1 -1
  84. data/app/views/upright/artifacts/show.html.erb +8 -0
  85. data/app/views/upright/dashboards/_uptime_probe_row.html.erb +5 -3
  86. data/app/views/upright/dashboards/probe_statuses/show.html.erb +1 -1
  87. data/app/views/upright/dashboards/uptimes/show.html.erb +1 -1
  88. data/app/views/upright/incidents/_form.html.erb +93 -0
  89. data/app/views/upright/incidents/_row.html.erb +15 -0
  90. data/app/views/upright/incidents/edit.html.erb +64 -0
  91. data/app/views/upright/incidents/index.html.erb +53 -0
  92. data/app/views/upright/incidents/new.html.erb +6 -0
  93. data/app/views/upright/probe_results/index.html.erb +7 -4
  94. data/app/views/upright/public/_branding.html.erb +3 -0
  95. data/app/views/upright/public/_footer.html.erb +3 -0
  96. data/app/views/upright/public/incidents/show.html.erb +29 -0
  97. data/app/views/upright/public/service_incidents/_incident.html.erb +14 -0
  98. data/app/views/upright/public/service_incidents/index.html.erb +50 -0
  99. data/app/views/upright/public/services/_active_events.html.erb +28 -0
  100. data/app/views/upright/public/services/_degraded_list.html.erb +14 -0
  101. data/app/views/upright/public/services/_overall_banner.html.erb +4 -0
  102. data/app/views/upright/public/services/_service.html.erb +34 -0
  103. data/app/views/upright/public/services/_upcoming_maintenance.html.erb +21 -0
  104. data/app/views/upright/public/services/_uptime_bar.html.erb +9 -0
  105. data/app/views/upright/public/services/index.html.erb +13 -0
  106. data/app/views/upright/public/services/index.rss.builder +29 -0
  107. data/app/views/upright/sessions/new.html.erb +14 -1
  108. data/config/ci.rb +2 -0
  109. data/config/importmap.rb +25 -14
  110. data/config/initializers/date_formats.rb +5 -0
  111. data/config/initializers/mime_types.rb +1 -0
  112. data/config/routes.rb +26 -2
  113. data/db/migrate/20260629000001_fix_probe_result_duration_precision.rb +14 -0
  114. data/db/persistent_migrate/20260512000001_create_upright_rollups.rb +16 -0
  115. data/db/persistent_migrate/20260702000001_create_upright_incidents.rb +34 -0
  116. data/db/persistent_migrate/20260707000001_add_probe_identity_to_upright_rollups.rb +11 -0
  117. data/db/persistent_migrate/20260709000001_add_created_by_to_incidents.rb +7 -0
  118. data/db/persistent_migrate/20260901000001_add_auto_reporting_to_upright_incidents.rb +22 -0
  119. data/lib/generators/upright/install/database_config.rb +46 -0
  120. data/lib/generators/upright/install/install_generator.rb +52 -3
  121. data/lib/generators/upright/install/templates/Dockerfile +80 -0
  122. data/lib/generators/upright/install/templates/content_security_policy.rb +42 -0
  123. data/lib/generators/upright/install/templates/deploy.yml +13 -13
  124. data/lib/generators/upright/install/templates/development_prometheus.yml +1 -1
  125. data/lib/generators/upright/install/templates/docker-compose.yml +12 -11
  126. data/lib/generators/upright/install/templates/http_probes.yml +1 -0
  127. data/lib/generators/upright/install/templates/omniauth.rb +7 -3
  128. data/lib/generators/upright/install/templates/otel_collector.yml +6 -0
  129. data/lib/generators/upright/install/templates/recurring.yml +45 -0
  130. data/lib/generators/upright/install/templates/sites.yml +4 -0
  131. data/lib/generators/upright/install/templates/smtp_probes.yml +3 -0
  132. data/lib/generators/upright/install/templates/traceroute_probes.yml +3 -0
  133. data/lib/generators/upright/install/templates/upright.rb +24 -2
  134. data/lib/generators/upright/install/templates/upright.rules.yml +14 -7
  135. data/lib/upright/configuration.rb +127 -6
  136. data/lib/upright/engine.rb +32 -2
  137. data/lib/upright/metrics.rb +27 -2
  138. data/lib/upright/probe_type_registry.rb +33 -0
  139. data/lib/upright/site.rb +31 -1
  140. data/lib/upright/version.rb +3 -1
  141. data/lib/upright.rb +34 -2
  142. metadata +110 -7
  143. data/config/credentials/development.key +0 -1
  144. data/config/credentials/test.key +0 -1
  145. data/lib/tasks/upright_tasks.rake +0 -4
@@ -2,6 +2,11 @@ class Upright::Configuration
2
2
  # Global subdomain is always "app" - this is documented behavior
3
3
  GLOBAL_SUBDOMAIN = "app"
4
4
 
5
+ # Public status pages live at status.<hostname>; custom CNAMEs match by subdomain.
6
+ PUBLIC_STATUS_SUBDOMAIN = "status"
7
+
8
+ DEFAULT_TRACE_VIEWER_URL = "https://trace.playwright.dev/"
9
+
5
10
  # Core settings
6
11
  attr_accessor :service_name
7
12
  attr_accessor :user_agent
@@ -10,6 +15,7 @@ class Upright::Configuration
10
15
  # Storage paths
11
16
  attr_accessor :prometheus_dir
12
17
  attr_accessor :video_storage_dir
18
+ attr_accessor :recording_base_dir
13
19
  attr_accessor :storage_state_dir
14
20
  attr_accessor :frozen_record_path
15
21
 
@@ -17,8 +23,13 @@ class Upright::Configuration
17
23
  attr_writer :probes_path
18
24
  attr_writer :authenticators_path
19
25
 
26
+ # Public status page services definition (env-overridable, like probes_path)
27
+ attr_writer :services_path
28
+
20
29
  # Playwright
21
- attr_accessor :playwright_server_url
30
+ attr_accessor :playwright_cli_path
31
+
32
+ attr_reader :trace_viewer_url
22
33
 
23
34
  # Authentication
24
35
  attr_accessor :auth_provider
@@ -29,6 +40,28 @@ class Upright::Configuration
29
40
  attr_accessor :prometheus_url
30
41
  attr_accessor :alert_webhook_url
31
42
 
43
+ attr_writer :proxy_token
44
+
45
+ attr_writer :rollup_minimum_coverage
46
+ attr_writer :rollup_evaluation_interval
47
+
48
+ # Probe types
49
+ attr_reader :probe_types
50
+
51
+ # Probe result cleanup
52
+ attr_accessor :stale_success_threshold
53
+ attr_accessor :stale_failure_threshold
54
+ attr_accessor :failure_retention_limit
55
+
56
+ # Public status pages
57
+ attr_accessor :public_status_enabled
58
+ attr_reader :public_status_custom_domains
59
+
60
+ # Extra stylesheets host apps layer on top of the engine's for the public
61
+ # status page (theming, branding). Logical asset names, loaded last so their
62
+ # :root overrides win the cascade.
63
+ attr_writer :public_stylesheets
64
+
32
65
  def initialize
33
66
  @service_name = "upright"
34
67
  @user_agent = "Upright/1.0"
@@ -36,16 +69,59 @@ class Upright::Configuration
36
69
 
37
70
  @prometheus_dir = nil
38
71
  @video_storage_dir = nil
72
+ @recording_base_dir = nil
39
73
  @storage_state_dir = nil
40
74
  @frozen_record_path = nil
41
75
  @probes_path = nil
42
76
  @authenticators_path = nil
77
+ @services_path = nil
43
78
 
44
- @playwright_server_url = ENV["PLAYWRIGHT_SERVER_URL"]
79
+ @probe_types = Upright::ProbeTypeRegistry.new
80
+
81
+ @playwright_cli_path = ENV.fetch("PLAYWRIGHT_CLI_PATH", "npx playwright")
45
82
  @otel_endpoint = ENV["OTEL_EXPORTER_OTLP_ENDPOINT"]
83
+ @prometheus_url = nil
46
84
 
47
85
  @auth_provider = :static_credentials
48
86
  @auth_options = {}
87
+
88
+ @stale_success_threshold = 24.hours
89
+ @stale_failure_threshold = 30.days
90
+ @failure_retention_limit = 20_000
91
+
92
+ self.trace_viewer_url = ENV.fetch("TRACE_VIEWER_URL", DEFAULT_TRACE_VIEWER_URL)
93
+
94
+ @public_status_enabled = false
95
+ @public_status_custom_domains = []
96
+ @public_stylesheets = nil
97
+ end
98
+
99
+ def trace_viewer_url=(url)
100
+ @trace_viewer_url = url.presence
101
+ host = trace_viewer_uri&.host
102
+
103
+ if @trace_viewer_url && (host.nil? || admin_domain?(host))
104
+ raise Upright::ConfigurationError, "config.trace_viewer_url must be an http(s) URL outside #{@hostname}"
105
+ end
106
+ end
107
+
108
+ def trace_viewer_origin
109
+ trace_viewer_uri&.then do |url|
110
+ "#{url.scheme}://#{url.host}#{":#{url.port}" unless url.port == url.default_port}"
111
+ end
112
+ end
113
+
114
+ def public_status_subdomain
115
+ PUBLIC_STATUS_SUBDOMAIN
116
+ end
117
+
118
+ def public_stylesheets
119
+ Array(@public_stylesheets)
120
+ end
121
+
122
+ def public_status_custom_domains=(domains)
123
+ @public_status_custom_domains = Array(domains)
124
+ configure_allowed_hosts if @hostname
49
125
  end
50
126
 
51
127
  def global_subdomain
@@ -60,10 +136,30 @@ class Upright::Configuration
60
136
  @prometheus_dir || Rails.root.join("tmp", "prometheus")
61
137
  end
62
138
 
139
+ def prometheus_url
140
+ @prometheus_url || ENV.fetch("PROMETHEUS_URL", "http://localhost:9090")
141
+ end
142
+
143
+ def proxy_token
144
+ @proxy_token || ENV["PROMETHEUS_OTLP_TOKEN"]
145
+ end
146
+
147
+ def rollup_minimum_coverage
148
+ @rollup_minimum_coverage || 0.9
149
+ end
150
+
151
+ def rollup_evaluation_interval
152
+ @rollup_evaluation_interval || 30.seconds
153
+ end
154
+
63
155
  def video_storage_dir
64
156
  @video_storage_dir || Rails.root.join("storage", "playwright_videos")
65
157
  end
66
158
 
159
+ def recording_base_dir
160
+ @recording_base_dir || Rails.root.join("storage", "playwright_recordings")
161
+ end
162
+
67
163
  def storage_state_dir
68
164
  @storage_state_dir || Rails.root.join("storage", "playwright_storage_states")
69
165
  end
@@ -76,6 +172,10 @@ class Upright::Configuration
76
172
  @probes_path || Rails.root.join("probes")
77
173
  end
78
174
 
175
+ def services_path
176
+ @services_path || Rails.root.join("config", "services.yml")
177
+ end
178
+
79
179
  def authenticators_path
80
180
  @authenticators_path || Rails.root.join("probes", "authenticators")
81
181
  end
@@ -90,17 +190,38 @@ class Upright::Configuration
90
190
  end
91
191
 
92
192
  def default_url_options
93
- if Rails.env.production?
94
- { protocol: "https", host: "#{global_subdomain}.#{hostname}", domain: hostname }
95
- else
193
+ if Rails.env.local?
96
194
  { protocol: "http", host: "#{global_subdomain}.#{hostname}", port: ENV.fetch("PORT", 3000).to_i, domain: hostname }
195
+ else
196
+ { protocol: "https", host: "#{global_subdomain}.#{hostname}", domain: hostname }
97
197
  end
98
198
  end
99
199
 
100
200
  private
201
+ def trace_viewer_uri
202
+ url = URI(@trace_viewer_url.to_s)
203
+ url if url.is_a?(URI::HTTP) && url.host.present?
204
+ rescue URI::InvalidURIError
205
+ nil
206
+ end
207
+
208
+ # DNS is case-insensitive and tolerates a trailing dot, so compare folded.
209
+ def admin_domain?(host)
210
+ return false if @hostname.blank?
211
+
212
+ host = host.downcase.chomp(".")
213
+ admin = @hostname.downcase
214
+
215
+ host == admin || host.end_with?(".#{admin}")
216
+ end
217
+
101
218
  def configure_allowed_hosts
102
219
  port_suffix = Rails.env.local? ? "(:\\d+)?" : ""
103
- Rails.application.config.hosts = [ /.*\.#{Regexp.escape(hostname)}#{port_suffix}/, /#{Regexp.escape(hostname)}#{port_suffix}/ ]
220
+ hosts = [ /.*\.#{Regexp.escape(hostname)}#{port_suffix}/, /#{Regexp.escape(hostname)}#{port_suffix}/ ]
221
+ Array(@public_status_custom_domains).each do |domain|
222
+ hosts << /\A#{Regexp.escape(domain)}#{port_suffix}\z/
223
+ end
224
+ Rails.application.config.hosts = hosts
104
225
  Rails.application.config.action_dispatch.tld_length = 1
105
226
  end
106
227
  end
@@ -6,14 +6,34 @@ class Upright::Engine < ::Rails::Engine
6
6
 
7
7
  # Session store configuration
8
8
  initializer "upright.session_store", before: :load_config_initializers do |app|
9
+ # Scope the cookie to the configured hostname (resolved per-request, since the
10
+ # host app sets it in an initializer that runs after this one) rather than
11
+ # `domain: :all`, which — with a multi-label hostname like upright.example.com
12
+ # — would hand the cookie to the registrable parent and every sibling under it
13
+ # (F-08). This is independent of tld_length, so custom status domains still
14
+ # parse normally.
9
15
  app.config.session_store :cookie_store,
10
16
  key: "_upright_session",
11
- domain: :all,
17
+ domain: ->(_request) { Upright.configuration.hostname&.then { |h| ".#{h}" } },
12
18
  same_site: :lax,
13
- secure: Rails.env.production?,
19
+ secure: !Rails.env.local?,
20
+ httponly: true,
14
21
  expire_after: 24.hours
15
22
  end
16
23
 
24
+ # The proxy token stands in for an admin session on the metrics proxies, so a
25
+ # blank one in a deployed environment is a fail-open: any bearer value would be
26
+ # compared against "" . Refuse to boot without one outside local envs — but not
27
+ # during `assets:precompile`, which Rails runs with SECRET_KEY_BASE_DUMMY and no
28
+ # runtime secrets (the Docker image builds before the token is available).
29
+ config.after_initialize do
30
+ building_assets = ENV["SECRET_KEY_BASE_DUMMY"].present?
31
+
32
+ if !Rails.env.local? && !building_assets && Upright.configuration.proxy_token.blank?
33
+ raise Upright::ConfigurationError, "Upright.configuration.proxy_token (or PROMETHEUS_OTLP_TOKEN) must be set outside development/test"
34
+ end
35
+ end
36
+
17
37
  config.after_initialize do
18
38
  url_options = Upright.configuration.default_url_options
19
39
  Rails.application.routes.default_url_options = url_options
@@ -56,6 +76,13 @@ class Upright::Engine < ::Rails::Engine
56
76
  end
57
77
  end
58
78
 
79
+ initializer "upright.probe_types", before: :load_config_initializers do
80
+ Upright.config.probe_types.register :http, name: "HTTP", icon: "🌐"
81
+ Upright.config.probe_types.register :playwright, name: "Playwright", icon: "🎭"
82
+ Upright.config.probe_types.register :smtp, name: "SMTP", icon: "✉️"
83
+ Upright.config.probe_types.register :traceroute, name: "Traceroute", icon: "🛤️"
84
+ end
85
+
59
86
  initializer "upright.frozen_record" do
60
87
  FrozenRecord::Base.base_path = Upright.configuration.frozen_record_path
61
88
  end
@@ -118,6 +145,9 @@ class Upright::Engine < ::Rails::Engine
118
145
  Upright.sites.each do |site|
119
146
  puts " #{site.city || site.code}: #{protocol}://#{site.code}.#{hostname}:#{port}"
120
147
  end
148
+ if Upright.configuration.public_status_enabled
149
+ puts " Public status: #{protocol}://#{Upright.configuration.public_status_subdomain}.#{hostname}:#{port}"
150
+ end
121
151
  puts ""
122
152
  end
123
153
  end
@@ -44,17 +44,42 @@ module Upright::Metrics
44
44
  gauge :probe_duration_seconds,
45
45
  comment: "Duration of each probe",
46
46
  aggregation: :max,
47
- tags: %i[type name probe_target probe_service status]
47
+ tags: %i[type name probe_target probe_service alert_severity status]
48
48
 
49
49
  gauge :probe_up,
50
50
  comment: "Probe status (1 = up, 0 = down)",
51
51
  aggregation: :most_recent,
52
- tags: %i[type name probe_target probe_service]
52
+ tags: %i[type name probe_target probe_service alert_severity]
53
53
 
54
54
  gauge :http_response_status,
55
55
  comment: "HTTP response status code",
56
56
  aggregation: :max,
57
57
  tags: %i[name probe_target probe_service]
58
+
59
+ gauge :service_under_maintenance,
60
+ comment: "Whether a service is under active maintenance (1 = yes, 0 = no)",
61
+ aggregation: :most_recent,
62
+ tags: %i[probe_service]
63
+
64
+ gauge :primary_site,
65
+ comment: "Whether this site is the primary (1 = yes, 0 = no)",
66
+ aggregation: :most_recent,
67
+ tags: []
68
+
69
+ gauge :persistent_db_up,
70
+ comment: "Whether the persistent database answers (1 = yes, 0 = no)",
71
+ aggregation: :most_recent,
72
+ tags: []
73
+
74
+ gauge :rollup_skipped_probes,
75
+ comment: "Probes the last rollup run left unwritten for insufficient Prometheus coverage",
76
+ aggregation: :most_recent,
77
+ tags: []
78
+
79
+ gauge :rollup_last_run_timestamp_seconds,
80
+ comment: "Unix time of the most recently written daily rollup",
81
+ aggregation: :max,
82
+ tags: []
58
83
  end
59
84
  end
60
85
  end
@@ -0,0 +1,33 @@
1
+ module Upright
2
+ class ProbeTypeRegistry
3
+ ProbeType = Data.define(:type, :name, :icon)
4
+
5
+ include Enumerable
6
+
7
+ def initialize
8
+ @probe_types = []
9
+ end
10
+
11
+ def register(type, name:, icon:)
12
+ type = type.to_s
13
+ @probe_types.reject! { |pt| pt.type == type }
14
+ @probe_types << ProbeType.new(type:, name:, icon:)
15
+ end
16
+
17
+ def types
18
+ @probe_types.map(&:type)
19
+ end
20
+
21
+ def find(type)
22
+ @probe_types.find { |pt| pt.type == type.to_s }
23
+ end
24
+
25
+ def unregister(type)
26
+ @probe_types.reject! { |pt| pt.type == type.to_s }
27
+ end
28
+
29
+ def each(&block)
30
+ @probe_types.each(&block)
31
+ end
32
+ end
33
+ end
data/lib/upright/site.rb CHANGED
@@ -2,19 +2,37 @@ module Upright
2
2
  class Site
3
3
  attr_reader :code, :city, :country, :geohash, :stagger_index
4
4
 
5
- def initialize(code:, city: nil, country: nil, geohash: nil, provider: nil, stagger_index: 0)
5
+ def initialize(code:, city: nil, country: nil, geohash: nil, provider: nil, stores_metrics: false, primary: false, stagger_index: 0)
6
6
  @code = code.to_sym
7
7
  @city = city
8
8
  @country = country
9
9
  @geohash = geohash
10
10
  @provider = provider
11
+ @stores_metrics = stores_metrics
12
+ @primary = primary
11
13
  @stagger_index = stagger_index
12
14
  end
13
15
 
16
+ def stores_metrics?
17
+ @stores_metrics
18
+ end
19
+
20
+ def primary?
21
+ @primary
22
+ end
23
+
14
24
  def host
15
25
  URI.parse(url).host
16
26
  end
17
27
 
28
+ def current?
29
+ code == Upright.current_site&.code
30
+ end
31
+
32
+ def prometheus_client
33
+ current? ? Upright.prometheus_client : peer_prometheus_client
34
+ end
35
+
18
36
  def provider
19
37
  @provider.to_s.inquiry
20
38
  end
@@ -40,6 +58,18 @@ module Upright
40
58
  end
41
59
 
42
60
  private
61
+ def peer_prometheus_client
62
+ Prometheus::ApiClient.client(url: prometheus_proxy_url, headers: proxy_authorization, options: { timeout: 30 })
63
+ end
64
+
65
+ def prometheus_proxy_url
66
+ "#{url.chomp("/")}/prometheus"
67
+ end
68
+
69
+ def proxy_authorization
70
+ { "Authorization" => "Bearer #{Upright.configuration.proxy_token}" }
71
+ end
72
+
43
73
  def coordinates
44
74
  @coordinates ||= Upright::Geohash.decode(geohash).first
45
75
  end
@@ -1,3 +1,5 @@
1
1
  module Upright
2
- VERSION = "0.2.0"
2
+ VERSION = "0.4.0"
3
+ # Keep in sync with the playwright-ruby-client constraint in upright.gemspec
4
+ PLAYWRIGHT_VERSION = "1.59"
3
5
  end
data/lib/upright.rb CHANGED
@@ -13,12 +13,14 @@ require "propshaft"
13
13
  require "importmap-rails"
14
14
  require "turbo-rails"
15
15
  require "stimulus-rails"
16
+ require "local_time"
16
17
  require "geared_pagination"
17
18
  require "yabeda/prometheus"
18
19
  require "yabeda/puma/plugin"
19
20
 
20
21
  require "upright/version"
21
22
  require "upright/configuration"
23
+ require "upright/probe_type_registry"
22
24
  require "upright/geohash"
23
25
  require "upright/site"
24
26
  require "upright/metrics"
@@ -38,6 +40,17 @@ module Upright
38
40
  yield(configuration)
39
41
  end
40
42
 
43
+ def probe_types
44
+ configuration.probe_types
45
+ end
46
+
47
+ def prometheus_client
48
+ Prometheus::ApiClient.client(
49
+ url: configuration.prometheus_url,
50
+ options: { timeout: 30.seconds }
51
+ )
52
+ end
53
+
41
54
  def sites
42
55
  @sites ||= load_sites
43
56
  end
@@ -47,7 +60,15 @@ module Upright
47
60
  end
48
61
 
49
62
  def current_site
50
- find_site(ENV["SITE_SUBDOMAIN"]) || sites.first
63
+ if (subdomain = ENV["SITE_SUBDOMAIN"]).present?
64
+ find_site(subdomain) || raise(ConfigurationError, "SITE_SUBDOMAIN=#{subdomain} is not a site in sites.yml (#{sites.map(&:code).join(', ')})")
65
+ else
66
+ sites.first
67
+ end
68
+ end
69
+
70
+ def primary_site
71
+ sites.find(&:primary?)
51
72
  end
52
73
 
53
74
  private
@@ -59,10 +80,21 @@ module Upright
59
80
 
60
81
  config[:sites].map.with_index do |site_config, index|
61
82
  Site.new(stagger_index: index, **site_config)
62
- end
83
+ end.tap { |sites| ensure_at_most_one_primary(sites) }
63
84
  else
64
85
  []
65
86
  end
66
87
  end
88
+
89
+ # Two primaries means two hosts running the singleton jobs that write the
90
+ # shared persistent database. None is how a host looks before it adopts
91
+ # the flag, so that stays legal.
92
+ def ensure_at_most_one_primary(sites)
93
+ primaries = sites.select(&:primary?)
94
+
95
+ if primaries.many?
96
+ raise ConfigurationError, "sites.yml declares #{primaries.count} primary sites (#{primaries.map(&:code).join(', ')}); at most one may be primary"
97
+ end
98
+ end
67
99
  end
68
100
  end