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
|
@@ -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 :
|
|
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
|
-
@
|
|
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.
|
|
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
|
-
|
|
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
|
data/lib/upright/engine.rb
CHANGED
|
@@ -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:
|
|
17
|
+
domain: ->(_request) { Upright.configuration.hostname&.then { |h| ".#{h}" } },
|
|
12
18
|
same_site: :lax,
|
|
13
|
-
secure: Rails.env.
|
|
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
|
data/lib/upright/metrics.rb
CHANGED
|
@@ -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
|
data/lib/upright/version.rb
CHANGED
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
|
-
|
|
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
|