upright 0.2.0 → 0.3.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +143 -50
  3. data/app/assets/stylesheets/upright/uptime-bars.css +10 -1
  4. data/app/controllers/upright/probe_results_controller.rb +1 -0
  5. data/app/controllers/upright/sessions_controller.rb +1 -0
  6. data/app/helpers/upright/probe_results_helper.rb +3 -10
  7. data/app/models/concerns/upright/playwright/form_authentication.rb +0 -3
  8. data/app/models/concerns/upright/probe_result/stale_cleanup.rb +23 -0
  9. data/app/models/concerns/upright/probeable.rb +7 -1
  10. data/app/models/upright/http/request.rb +1 -1
  11. data/app/models/upright/probe_result.rb +6 -1
  12. data/app/models/upright/probes/status.rb +3 -3
  13. data/app/models/upright/probes/uptime.rb +4 -4
  14. data/app/views/upright/dashboards/_uptime_probe_row.html.erb +5 -3
  15. data/app/views/upright/dashboards/probe_statuses/show.html.erb +1 -1
  16. data/app/views/upright/dashboards/uptimes/show.html.erb +1 -1
  17. data/app/views/upright/probe_results/index.html.erb +7 -4
  18. data/config/ci.rb +2 -0
  19. data/lib/generators/upright/install/install_generator.rb +43 -2
  20. data/lib/generators/upright/install/templates/http_probes.yml +1 -0
  21. data/lib/generators/upright/install/templates/recurring.yml +25 -0
  22. data/lib/generators/upright/install/templates/smtp_probes.yml +3 -0
  23. data/lib/generators/upright/install/templates/traceroute_probes.yml +3 -0
  24. data/lib/generators/upright/install/templates/upright.rb +3 -0
  25. data/lib/generators/upright/install/templates/upright.rules.yml +5 -5
  26. data/lib/upright/configuration.rb +14 -0
  27. data/lib/upright/engine.rb +7 -0
  28. data/lib/upright/metrics.rb +2 -2
  29. data/lib/upright/probe_type_registry.rb +33 -0
  30. data/lib/upright/version.rb +1 -1
  31. data/lib/upright.rb +5 -0
  32. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d1bb4a1f14f8e4dc16cf5bebcd5868917b2c0703f6dd94592d6f8c818dfa0b9
4
- data.tar.gz: 4d3a0593d1f49f256145340f3e64c9ab6f467a759f1c0a60e81e58635528e0d1
3
+ metadata.gz: ccb561e794bdafa5e31caed2347f7d371d02e5230ca875c5f507d1d0620d7363
4
+ data.tar.gz: 3bc20100e850cdd93a81e58b7e9b6f3ef9377c44b17bbd7891dbe697699b23a6
5
5
  SHA512:
6
- metadata.gz: 5d06f946c9874af94e42bca38bb58671e227b3c4f2ac2f9a523d36b3efca04771a3d0a087bb2644ea067e5a8094d35e1370ee85ded8ea140af084afbf4a4e29a
7
- data.tar.gz: c2ac9d3d2822197887e22308dbdce7be1b47692825dcb25cbb660f0de8e24643788b2cd5a63c72c4863041b619a35e3b31b9e241c69861b59ff66c8398fcd9b2
6
+ metadata.gz: 474970b3ccbce05a396ceb2a3575a58272b602d679cf70d55d10538cc546aa31d6142e1de401ee5098c6c3a2733a624cba54e34b79a0dcd9c6af7ad7e8124f0d
7
+ data.tar.gz: 35ed0971379b1c50dd9583f95ed27aa2586f53002e8acd81c05851797d6f2d1c59ced8d6a08817613ecb89ab376d453a9697cf241b82be1409c11f1caa87c6d3
data/README.md CHANGED
@@ -47,7 +47,8 @@ Upright is a self-hosted synthetic monitoring system. It provides a framework fo
47
47
 
48
48
  ## Installation
49
49
 
50
- Upright is designed to be run in it's own Rails app and deployed with Kamal.
50
+ > [!NOTE]
51
+ > Upright is designed to be run in its own Rails app and deployed with Kamal.
51
52
 
52
53
  ### Quick Start (New Project)
53
54
 
@@ -157,6 +158,91 @@ Upright.configure do |config|
157
158
  end
158
159
  ```
159
160
 
161
+ ### Probe Result Cleanup
162
+
163
+ Upright automatically cleans up old probe results on a recurring schedule. You can configure the retention thresholds:
164
+
165
+ ```ruby
166
+ Upright.configure do |config|
167
+ config.stale_success_threshold = 24.hours # Delete successful results older than this (default: 24 hours)
168
+ config.stale_failure_threshold = 30.days # Delete failed results older than this (default: 30 days)
169
+ config.failure_retention_limit = 20_000 # Keep at most this many failed results (default: 20,000)
170
+ end
171
+ ```
172
+
173
+ ## Custom Probe Types
174
+
175
+ Upright ships with four built-in probe types: HTTP, Playwright, SMTP, and Traceroute. You can register your own to extend the system.
176
+
177
+ ### 1. Register the type
178
+
179
+ Add it to your initializer so Upright knows about its name and icon:
180
+
181
+ ```ruby
182
+ # config/initializers/upright.rb
183
+ Upright.configure do |config|
184
+ config.probe_types.register :ping, name: "Ping", icon: "📶"
185
+ end
186
+ ```
187
+
188
+ ### 2. Create the probe class
189
+
190
+ Add a Ruby class in your `probes/` directory that extends `FrozenRecord::Base` and includes `Upright::Probeable` and `Upright::ProbeYamlSource`. Implement `probe_type`, `probe_target`, `check`, and `on_check_recorded`:
191
+
192
+ ```ruby
193
+ # probes/ping_probe.rb
194
+ class PingProbe < FrozenRecord::Base
195
+ include Upright::Probeable
196
+ include Upright::ProbeYamlSource
197
+
198
+ stagger_by_site 3.seconds
199
+
200
+ def check
201
+ @ping_output, status = Open3.capture2e("ping", "-c", "1", "-W", "5", host)
202
+ status.success?
203
+ end
204
+
205
+ def on_check_recorded(probe_result)
206
+ if @ping_output.present?
207
+ Upright::Artifact.new(name: "ping.log", content: @ping_output).attach_to(probe_result)
208
+ end
209
+ end
210
+
211
+ def probe_type = "ping"
212
+ def probe_target = host
213
+ end
214
+ ```
215
+
216
+ The `check` method should return a truthy value on success and a falsy value on failure.
217
+
218
+ ### 3. Define probes in YAML
219
+
220
+ Create a YAML file matching the class name (e.g., `PingProbe` → `probes/ping_probes.yml`):
221
+
222
+ ```yaml
223
+ # probes/ping_probes.yml
224
+ - name: "Cloudflare DNS"
225
+ host: "1.1.1.1"
226
+
227
+ - name: "Google DNS"
228
+ host: "8.8.8.8"
229
+ ```
230
+
231
+ Fields defined in the YAML are available as methods on the probe instance (e.g., `host`).
232
+
233
+ ### 4. Schedule it
234
+
235
+ Add a recurring job in `config/recurring.yml`:
236
+
237
+ ```yaml
238
+ production:
239
+ ping_probes:
240
+ command: "PingProbe.check_and_record_all_later"
241
+ schedule: every 30 seconds
242
+ ```
243
+
244
+ The custom type will automatically appear in all dashboard dropdowns and filter links.
245
+
160
246
  ## Defining Probes
161
247
 
162
248
  ### HTTP Probes
@@ -171,12 +257,15 @@ Add probes to `probes/http_probes.yml`:
171
257
  - name: API Health
172
258
  url: https://api.example.com/health
173
259
  expected_status: 200
260
+ alert_severity: critical
174
261
 
175
262
  - name: Admin Panel
176
263
  url: https://admin.example.com
177
264
  basic_auth_credentials: admin_auth # Key in Rails credentials
178
265
  ```
179
266
 
267
+ The optional `alert_severity` field controls the Prometheus alert severity when a probe fails. Values: `medium`, `high` (default), `critical`.
268
+
180
269
  ### SMTP Probes
181
270
 
182
271
  Add probes to `probes/smtp_probes.yml`:
@@ -317,70 +406,82 @@ image: your-org/upright
317
406
  servers:
318
407
  web:
319
408
  hosts:
320
- - nyc.upright.example.com
321
- env:
322
- tags:
323
- SITE_SUBDOMAIN: nyc
324
-
325
- hosts:
326
- - ams.upright.example.com
327
- env:
328
- tags:
329
- SITE_SUBDOMAIN: ams
330
-
409
+ - ams.upright.example.com: [amsterdam]
410
+ - nyc.upright.example.com: [new_york]
411
+ - sfo.upright.example.com: [san_francisco]
412
+ jobs:
331
413
  hosts:
332
- - sfo.upright.example.com
333
- env:
334
- tags:
335
- SITE_SUBDOMAIN: sfo
414
+ - ams.upright.example.com: [amsterdam]
415
+ - nyc.upright.example.com: [new_york]
416
+ - sfo.upright.example.com: [san_francisco]
417
+ cmd: bin/jobs
336
418
 
337
- registry:
338
- server: ghcr.io
339
- username: your-org
340
- password:
341
- - KAMAL_REGISTRY_PASSWORD
419
+ proxy:
420
+ app_port: 3000
421
+ ssl: true
422
+ hosts:
423
+ - "*.upright.example.com"
342
424
 
343
425
  env:
344
- clear:
345
- RAILS_ENV: production
346
- RAILS_LOG_TO_STDOUT: true
347
426
  secret:
348
427
  - RAILS_MASTER_KEY
349
- - OIDC_CLIENT_ID
350
- - OIDC_CLIENT_SECRET
428
+ tags:
429
+ amsterdam:
430
+ SITE_SUBDOMAIN: ams
431
+ new_york:
432
+ SITE_SUBDOMAIN: nyc
433
+ san_francisco:
434
+ SITE_SUBDOMAIN: sfo
351
435
 
352
436
  accessories:
353
437
  playwright:
354
- image: mcr.microsoft.com/playwright:v1.55.0-noble
355
- cmd: npx -y playwright run-server --port 53333
356
- host: nyc.upright.example.com
357
- port: 53333
358
- env:
359
- clear:
360
- DEBUG: pw:api
438
+ image: jacoblincool/playwright:chromium-server-1.55.0
439
+ port: "127.0.0.1:53333:53333"
440
+ roles:
441
+ - jobs
442
+
443
+ prometheus:
444
+ image: prom/prometheus:v3.2.1
445
+ hosts:
446
+ - ams.upright.example.com
447
+ cmd: >-
448
+ --config.file=/etc/prometheus/prometheus.yml
449
+ --storage.tsdb.path=/prometheus
450
+ --storage.tsdb.retention.time=30d
451
+ --web.enable-otlp-receiver
452
+ files:
453
+ - config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
454
+ - config/prometheus/rules/upright.rules.yml:/etc/prometheus/rules/upright.rules.yml
455
+
456
+ alertmanager:
457
+ image: prom/alertmanager:v0.28.1
458
+ hosts:
459
+ - ams.upright.example.com
460
+ cmd: --config.file=/etc/alertmanager/alertmanager.yml
461
+ files:
462
+ - config/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
361
463
  ```
362
464
 
363
465
  ## Observability
364
466
 
365
467
  ### Prometheus
366
468
 
367
- The engine exposes metrics at `/metrics`. Configure Prometheus to scrape:
469
+ Metrics are exposed via a Puma plugin at `http://0.0.0.0:9394/metrics`. Configure Prometheus to scrape:
368
470
 
369
471
  ```yaml
370
472
  scrape_configs:
371
473
  - job_name: upright
372
474
  static_configs:
373
- - targets: ['localhost:3000']
374
- metrics_path: /metrics
475
+ - targets: ['localhost:9394']
375
476
  ```
376
477
 
377
478
  ### Metrics Exposed
378
479
 
379
480
  - `upright_probe_duration_seconds` - Probe execution duration
380
- - `upright_probe_success` - Probe success/failure (1/0)
381
- - `upright_probe_status_code` - HTTP status code for HTTP probes
481
+ - `upright_probe_up` - Probe status (1 = up, 0 = down)
482
+ - `upright_http_response_status` - HTTP response status code
382
483
 
383
- Labels include: `probe_name`, `probe_type`, `site_code`, `site_city`, `site_country`
484
+ Labels include: `type`, `name`, `site_code`, `site_city`, `site_country`
384
485
 
385
486
  ### AlertManager
386
487
 
@@ -391,20 +492,12 @@ groups:
391
492
  - name: upright
392
493
  rules:
393
494
  - alert: ProbeDown
394
- expr: upright_probe_success == 0
395
- for: 5m
396
- labels:
397
- severity: critical
398
- annotations:
399
- summary: "Probe {{ $labels.probe_name }} is down"
400
-
401
- - alert: ProbeSlow
402
- expr: upright_probe_duration_seconds > 10
495
+ expr: upright_probe_up == 0
403
496
  for: 5m
404
497
  labels:
405
- severity: warning
498
+ severity: "{{ $labels.alert_severity }}"
406
499
  annotations:
407
- summary: "Probe {{ $labels.probe_name }} is slow"
500
+ summary: "Probe {{ $labels.name }} is down"
408
501
  ```
409
502
 
410
503
  ### OpenTelemetry
@@ -40,7 +40,6 @@
40
40
  /* Individual uptime bar */
41
41
  .uptime-bar {
42
42
  border-radius: 2px;
43
- cursor: default;
44
43
  flex: 1;
45
44
  height: 24px;
46
45
  max-width: 12px;
@@ -48,6 +47,16 @@
48
47
  transition: transform 100ms, filter 100ms;
49
48
  }
50
49
 
50
+ a.uptime-bar {
51
+ cursor: pointer;
52
+ display: block;
53
+ text-decoration: none;
54
+ }
55
+
56
+ div.uptime-bar {
57
+ cursor: default;
58
+ }
59
+
51
60
  .uptime-bar:hover {
52
61
  filter: brightness(1.1);
53
62
  transform: scaleY(1.15);
@@ -12,6 +12,7 @@ class Upright::ProbeResultsController < Upright::ApplicationController
12
12
  .by_type(params[:probe_type])
13
13
  .by_status(params[:status])
14
14
  .by_name(params[:probe_name])
15
+ .by_date(params[:date])
15
16
  .with_attached_artifacts
16
17
  end
17
18
  end
@@ -8,6 +8,7 @@ class Upright::SessionsController < Upright::ApplicationController
8
8
  end
9
9
 
10
10
  def create
11
+ reset_session
11
12
  user = Upright::User.from_omniauth(request.env["omniauth.auth"])
12
13
  session[:user_info] = { email: user.email, name: user.name }
13
14
  redirect_to upright.root_path
@@ -1,15 +1,7 @@
1
1
  module Upright::ProbeResultsHelper
2
- PROBE_TYPE_ICONS = {
3
- http: "🌐",
4
- playwright: "🎭",
5
- ping: "📶",
6
- smtp: "✉️",
7
- traceroute: "🛤️"
8
- }
9
-
10
2
  def probe_type_icon(probe_type)
11
- icon = PROBE_TYPE_ICONS.fetch(probe_type.to_s.downcase.to_sym)
12
- content_tag(:span, icon, title: probe_type.titleize)
3
+ registered = Upright.probe_types.find(probe_type)
4
+ content_tag(:span, registered.icon, title: registered.name)
13
5
  end
14
6
 
15
7
  def type_filter_link(label, probe_type = nil)
@@ -44,6 +36,7 @@ module Upright::ProbeResultsHelper
44
36
  parts << "for #{params[:probe_type].titleize} probes" if params[:probe_type].present?
45
37
  parts << "named #{params[:probe_name]}" if params[:probe_name].present?
46
38
  parts << "with status #{params[:status]}" if params[:status].present?
39
+ parts << "on #{Date.parse(params[:date]).to_fs(:long)}" if params[:date].present?
47
40
  parts.join(" ")
48
41
  end
49
42
  end
@@ -19,9 +19,6 @@ module Upright::Playwright::FormAuthentication
19
19
  end
20
20
 
21
21
  def authenticator_for(service)
22
- # First try the host app's authenticator, then fall back to engine's
23
22
  "::Playwright::Authenticator::#{service.to_s.camelize}".constantize
24
- rescue NameError
25
- "Upright::Playwright::Authenticator::#{service.to_s.camelize}".constantize
26
23
  end
27
24
  end
@@ -0,0 +1,23 @@
1
+ module Upright::ProbeResult::StaleCleanup
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def cleanup_stale
6
+ cleanup_stale_successes
7
+ cleanup_stale_failures
8
+ end
9
+
10
+ def cleanup_stale_successes
11
+ ok.where(created_at: ...Upright.config.stale_success_threshold.ago).in_batches.destroy_all
12
+ end
13
+
14
+ def cleanup_stale_failures
15
+ cutoff = [
16
+ Upright.config.stale_failure_threshold.ago,
17
+ fail.order(created_at: :desc).offset(Upright.config.failure_retention_limit).pick(:created_at)
18
+ ].compact.max
19
+
20
+ fail.where(created_at: ..cutoff).in_batches.destroy_all
21
+ end
22
+ end
23
+ end
@@ -2,7 +2,7 @@ module Upright::Probeable
2
2
  extend ActiveSupport::Concern
3
3
  include Upright::Staggerable
4
4
 
5
- TYPES = %w[ http playwright smtp traceroute ]
5
+ ALERT_SEVERITIES = %i[ medium high critical ]
6
6
 
7
7
  included do
8
8
  attr_writer :logger
@@ -35,6 +35,7 @@ module Upright::Probeable
35
35
  probe_name: probe_name,
36
36
  probe_target: probe_target,
37
37
  probe_service: probe_service,
38
+ probe_alert_severity: probe_alert_severity,
38
39
  status: result[:status],
39
40
  duration: result[:duration],
40
41
  error: result[:error]
@@ -63,6 +64,11 @@ module Upright::Probeable
63
64
  nil
64
65
  end
65
66
 
67
+ def probe_alert_severity
68
+ severity = try(:alert_severity)&.to_sym
69
+ ALERT_SEVERITIES.include?(severity) ? severity : :high
70
+ end
71
+
66
72
  private
67
73
  def failsafe_check
68
74
  result, error, duration = nil
@@ -23,7 +23,7 @@ class Upright::HTTP::Request
23
23
  proxy: proxy_url,
24
24
  proxyuserpwd: proxy_userpwd,
25
25
  verbose: true,
26
- forbid_reuse: true
26
+ forbid_reuse: proxy_url.nil?
27
27
  }.compact
28
28
  end
29
29
 
@@ -1,11 +1,16 @@
1
1
  class Upright::ProbeResult < Upright::ApplicationRecord
2
2
  include Upright::ExceptionRecording
3
+ include Upright::ProbeResult::StaleCleanup
4
+
5
+ attr_accessor :probe_alert_severity
3
6
 
4
7
  has_many_attached :artifacts
5
8
 
6
9
  scope :by_type, ->(type) { where(probe_type: type) if type.present? }
7
10
  scope :by_status, ->(status) { where(status: status) if status.present? }
8
11
  scope :by_name, ->(name) { where(probe_name: name) if name.present? }
12
+ scope :by_date, ->(date) { where(created_at: Date.parse(date).all_day) if date.present? }
13
+
9
14
  scope :stale, -> { where(created_at: ...24.hours.ago) }
10
15
 
11
16
  enum :status, [ :ok, :fail ]
@@ -23,7 +28,7 @@ class Upright::ProbeResult < Upright::ApplicationRecord
23
28
 
24
29
  private
25
30
  def increment_metrics
26
- labels = { type: probe_type, name: probe_name, probe_target: probe_target, probe_service: probe_service }
31
+ labels = { type: probe_type, name: probe_name, probe_target: probe_target, probe_service: probe_service, alert_severity: probe_alert_severity || :high }
27
32
 
28
33
  Yabeda.upright_probe_duration_seconds.set(labels.merge(status: status), duration.to_f)
29
34
  Yabeda.upright_probe_up.set(labels, ok? ? 1 : 0)
@@ -17,9 +17,9 @@ class Upright::Probes::Status
17
17
  end
18
18
 
19
19
  def label_selector(probe_type)
20
- if probe_type.present?
21
- "{type=\"#{probe_type}\"}"
22
- end
20
+ matchers = [ "alert_severity!=\"\"" ]
21
+ matchers << "type=\"#{probe_type}\"" if probe_type.present?
22
+ "{#{matchers.join(",")}}"
23
23
  end
24
24
 
25
25
  def prometheus_client
@@ -17,13 +17,13 @@ class Upright::Probes::Uptime
17
17
 
18
18
  private
19
19
  def query(probe_type)
20
- "upright:probe_uptime_daily#{label_selector(probe_type)}"
20
+ "min by (name, type, probe_target) (upright:probe_uptime_daily#{label_selector(probe_type)})"
21
21
  end
22
22
 
23
23
  def label_selector(probe_type)
24
- if probe_type.present?
25
- "{type=\"#{probe_type}\"}"
26
- end
24
+ matchers = [ "alert_severity!=\"\"" ]
25
+ matchers << "type=\"#{probe_type}\"" if probe_type.present?
26
+ "{#{matchers.join(",")}}"
27
27
  end
28
28
 
29
29
  def prometheus_client
@@ -1,5 +1,5 @@
1
1
  <div class="data-table__row uptime-bars__row">
2
- <%= link_to upright.site_root_url(subdomain: Upright.sites.first.code, probe_type: probe.type, probe_name: probe.name), class: "data-table__probe" do %>
2
+ <%= link_to upright.site_root_url(subdomain: current_or_default_site.code, probe_type: probe.type, probe_name: probe.name), class: "data-table__probe" do %>
3
3
  <%= probe_type_icon(probe.type) %>
4
4
  <span class="data-table__probe-name"><%= probe.name %></span>
5
5
  <% end %>
@@ -11,8 +11,10 @@
11
11
  <% else %>
12
12
  <% uptime_percent = uptime * 100 %>
13
13
  <% downtime_minutes = ((1 - uptime) * 24 * 60).round %>
14
- <div class="uptime-bar uptime-bar--<%= uptime_label(uptime_percent) %>"
15
- title="<%= uptime_bar_tooltip(date, uptime_percent, downtime_minutes) %>"></div>
14
+ <%= link_to "", upright.site_root_url(subdomain: current_or_default_site.code, probe_type: probe.type, probe_name: probe.name, date: date.iso8601),
15
+ class: "uptime-bar uptime-bar--#{uptime_label(uptime_percent)}",
16
+ title: uptime_bar_tooltip(date, uptime_percent, downtime_minutes),
17
+ aria: { label: uptime_bar_tooltip(date, uptime_percent, downtime_minutes) } %>
16
18
  <% end %>
17
19
  <% end %>
18
20
  </div>
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div class="dashboard-filters">
6
6
  <%= form_with url: dashboards_probe_status_path, method: :get, data: { controller: "form", turbo_frame: :probe_status_matrix } do %>
7
- <%= collection_select nil, :probe_type, Upright::Probeable::TYPES, :itself, :titleize,
7
+ <%= collection_select nil, :probe_type, Upright.probe_types, :type, :name,
8
8
  { selected: @probe_type },
9
9
  { class: "input--select", data: { action: "change->form#submit" } } %>
10
10
  <% end %>
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div class="dashboard-filters">
6
6
  <%= form_with url: dashboards_uptime_path, method: :get, data: { controller: "form", turbo_frame: :uptime_bars } do %>
7
- <%= collection_select nil, :probe_type, Upright::Probeable::TYPES, :itself, :titleize,
7
+ <%= collection_select nil, :probe_type, Upright.probe_types, :type, :name,
8
8
  { selected: @probe_type },
9
9
  { class: "input--select", data: { action: "change->form#submit" } } %>
10
10
  <% end %>
@@ -2,10 +2,9 @@
2
2
  <aside>
3
3
  <ul>
4
4
  <li><%= type_filter_link "All" %></li>
5
- <li><%= type_filter_link "HTTP", "http" %></li>
6
- <li><%= type_filter_link "Playwright", "playwright" %></li>
7
- <li><%= type_filter_link "SMTP", "smtp" %></li>
8
- <li><%= type_filter_link "Traceroute", "traceroute" %></li>
5
+ <% Upright.probe_types.each do |probe_type| %>
6
+ <li><%= type_filter_link probe_type.name, probe_type.type %></li>
7
+ <% end %>
9
8
  </ul>
10
9
  </aside>
11
10
 
@@ -16,6 +15,7 @@
16
15
  <div class="filters">
17
16
  <%= form_with url: site_root_path, method: :get, data: { controller: "form" } do |f| %>
18
17
  <%= hidden_field_tag :probe_type, params[:probe_type] if params[:probe_type].present? %>
18
+ <%= hidden_field_tag :date, params[:date] if params[:date].present? %>
19
19
  <label for="probe_name">Name</label>
20
20
  <%= select_tag :probe_name,
21
21
  options_for_select([["All", ""]] + @probe_names, params[:probe_name]),
@@ -26,6 +26,9 @@
26
26
  options_for_select([["All", ""]] + Upright::ProbeResult.statuses.keys.map { |s| [s.titleize, s] }, params[:status]),
27
27
  class: "input--select",
28
28
  data: { action: "change->form#submit" } %>
29
+ <% if params[:date].present? %>
30
+ <%= link_to "Clear date filter", site_root_path(probe_type: params[:probe_type], probe_name: params[:probe_name], status: params[:status]), class: "filter-clear" %>
31
+ <% end %>
29
32
  <% end %>
30
33
  </div>
31
34
 
data/config/ci.rb CHANGED
@@ -3,5 +3,7 @@
3
3
  CI.run do
4
4
  step "Style: Ruby", "bin/rubocop"
5
5
  step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
6
+ step "Lint: GitHub Actions (actionlint)", "actionlint"
7
+ step "Lint: GitHub Actions (zizmor)", "zizmor ."
6
8
  step "Tests: Rails", "bin/rails test"
7
9
  end
@@ -5,6 +5,15 @@ module Upright
5
5
 
6
6
  desc "Install Upright engine into your application"
7
7
 
8
+ def set_ruby_version
9
+ ruby_version = RUBY_VERSION
10
+ create_file ".ruby-version", "#{ruby_version}\n"
11
+ create_file "mise.toml", <<~TOML
12
+ [tools]
13
+ ruby = "#{ruby_version}"
14
+ TOML
15
+ end
16
+
8
17
  def copy_initializers
9
18
  template "upright.rb", "config/initializers/upright.rb"
10
19
  template "omniauth.rb", "config/initializers/omniauth.rb"
@@ -43,6 +52,38 @@ module Upright
43
52
  template "puma.rb", "config/puma.rb"
44
53
  end
45
54
 
55
+ def install_solid_queue
56
+ rails_command "solid_queue:install"
57
+ end
58
+
59
+ def add_queue_database
60
+ gsub_file "config/database.yml",
61
+ "development:\n <<: *default\n database: storage/development.sqlite3",
62
+ "development:\n primary:\n <<: *default\n database: storage/development.sqlite3\n queue:\n <<: *default\n database: storage/development_queue.sqlite3\n migrations_paths: db/queue_migrate"
63
+
64
+ gsub_file "config/database.yml",
65
+ "test:\n <<: *default\n database: storage/test.sqlite3",
66
+ "test:\n primary:\n <<: *default\n database: storage/test.sqlite3\n queue:\n <<: *default\n database: storage/test_queue.sqlite3\n migrations_paths: db/queue_migrate"
67
+ end
68
+
69
+ def copy_recurring_config
70
+ template "recurring.yml", "config/recurring.yml", force: true
71
+ end
72
+
73
+ def add_jobs_to_procfile
74
+ procfile = File.join(destination_root, "Procfile.dev")
75
+ if File.exist?(procfile)
76
+ unless File.read(procfile).include?("jobs:")
77
+ append_to_file "Procfile.dev", "jobs: OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES bin/rails solid_queue:start\n"
78
+ end
79
+ else
80
+ create_file "Procfile.dev", <<~PROCFILE
81
+ web: bin/rails server -b '0.0.0.0' -p ${PORT:-3000}
82
+ jobs: OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES bin/rails solid_queue:start
83
+ PROCFILE
84
+ end
85
+ end
86
+
46
87
  def add_routes
47
88
  route 'mount Upright::Engine => "/", as: :upright'
48
89
  end
@@ -60,14 +101,14 @@ module Upright
60
101
  say "Upright has been installed!", :green
61
102
  say ""
62
103
  say "Next steps:"
63
- say " 1. Run migrations: bin/rails db:migrate"
104
+ say " 1. Prepare the database: bin/rails db:prepare"
64
105
  say " 2. Configure your servers in config/deploy.yml"
65
106
  say " 3. Configure sites in config/sites.yml"
66
107
  say " 4. Add probes in probes/*.yml"
67
108
  say " 5. Set ADMIN_PASSWORD env var (default: upright)"
68
109
  say ""
69
110
  say "For production, review config/initializers/upright.rb and update:"
70
- say " config.hostname = \"honcho-upright.com\""
111
+ say " config.hostname = \"example.com\""
71
112
  say ""
72
113
  say "Start dev services (Prometheus, Alertmanager, Playwright):"
73
114
  say " docker compose up -d"
@@ -7,6 +7,7 @@
7
7
  #
8
8
  # Optional fields:
9
9
  # - expected_status: HTTP status code to expect (default: 200-399)
10
+ # - alert_severity: Alert severity when probe fails: medium, high (default), critical
10
11
  # - basic_auth_credentials: Key in Rails credentials for basic auth
11
12
  # - proxy: Key in Rails credentials for proxy settings
12
13
 
@@ -0,0 +1,25 @@
1
+ development:
2
+ http_probes:
3
+ schedule: every minute
4
+ command: "Upright::Probes::HTTPProbe.check_and_record_all_later"
5
+
6
+ smtp_probes:
7
+ schedule: every minute
8
+ command: "Upright::Probes::SMTPProbe.check_and_record_all_later"
9
+
10
+ production:
11
+ http_probes:
12
+ schedule: "*/30 * * * * *"
13
+ command: "Upright::Probes::HTTPProbe.check_and_record_all_later"
14
+
15
+ smtp_probes:
16
+ schedule: "*/30 * * * * *"
17
+ command: "Upright::Probes::SMTPProbe.check_and_record_all_later"
18
+
19
+ clear_solid_queue_finished_jobs:
20
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
21
+ schedule: every hour at minute 12
22
+
23
+ cleanup_stale_probe_results:
24
+ command: "Upright::ProbeResult.cleanup_stale"
25
+ schedule: every hour at minute 30
@@ -4,6 +4,9 @@
4
4
  # Required fields:
5
5
  # - name: Unique identifier for the probe
6
6
  # - host: SMTP server hostname
7
+ #
8
+ # Optional fields:
9
+ # - alert_severity: Alert severity when probe fails: medium, high (default), critical
7
10
 
8
11
  # - name: Example Mail Server
9
12
  # host: mail.example.com
@@ -4,6 +4,9 @@
4
4
  # Required fields:
5
5
  # - name: Unique identifier for the probe
6
6
  # - host: Hostname or IP address to trace
7
+ #
8
+ # Optional fields:
9
+ # - alert_severity: Alert severity when probe fails: medium, high (default), critical
7
10
 
8
11
  # - name: Example Service
9
12
  # host: example.com
@@ -18,4 +18,7 @@ Upright.configure do |config|
18
18
  # client_id: ENV["OIDC_CLIENT_ID"],
19
19
  # client_secret: ENV["OIDC_CLIENT_SECRET"]
20
20
  # }
21
+
22
+ # Register custom probe types (built-in types: http, playwright, smtp, traceroute)
23
+ # config.probe_types.register :ftp_file, name: "FTP File", icon: "📂"
21
24
  end
@@ -8,9 +8,9 @@ groups:
8
8
  # Fraction of regions reporting DOWN (0.0 to 1.0)
9
9
  - record: upright:probe_down_fraction
10
10
  expr: |
11
- count by (name, type, probe_target) (upright_probe_up == 0)
11
+ count by (name, type, probe_target, alert_severity) (upright_probe_up == 0)
12
12
  /
13
- count by (name, type, probe_target) (upright_probe_up)
13
+ count by (name, type, probe_target, alert_severity) (upright_probe_up)
14
14
 
15
15
  # Daily uptime percentage (0.0 to 1.0)
16
16
  # Uptime = percentage of time in past day when majority of sites reported UP
@@ -148,7 +148,7 @@ groups:
148
148
  upright: "https://app.<%= app_domain %>/?probe_type={{ $labels.type }}&status=fail&probe_name={{ $labels.name | urlquery }}"
149
149
  expr: upright:probe_down_fraction{type="http"} > 0.5
150
150
  labels:
151
- severity: page
151
+ severity: "{{ $labels.alert_severity }}"
152
152
  group: upright
153
153
 
154
154
  - alert: UprightHTTPProbeDegraded
@@ -187,7 +187,7 @@ groups:
187
187
  upright: "https://app.<%= app_domain %>/?probe_type={{ $labels.type }}&status=fail&probe_name={{ $labels.name | urlquery }}"
188
188
  expr: upright:probe_down_fraction{type="smtp"} > 0.5
189
189
  labels:
190
- severity: page
190
+ severity: "{{ $labels.alert_severity }}"
191
191
  group: upright
192
192
 
193
193
  - alert: UprightSMTPProbeDegraded
@@ -226,7 +226,7 @@ groups:
226
226
  upright: "https://app.<%= app_domain %>/?probe_type={{ $labels.type }}&status=fail&probe_name={{ $labels.name | urlquery }}"
227
227
  expr: upright:probe_down_fraction{type="playwright"} > 0.5
228
228
  labels:
229
- severity: page
229
+ severity: "{{ $labels.alert_severity }}"
230
230
  group: upright
231
231
 
232
232
  - alert: UprightPlaywrightProbeDegraded
@@ -29,6 +29,14 @@ class Upright::Configuration
29
29
  attr_accessor :prometheus_url
30
30
  attr_accessor :alert_webhook_url
31
31
 
32
+ # Probe types
33
+ attr_reader :probe_types
34
+
35
+ # Probe result cleanup
36
+ attr_accessor :stale_success_threshold
37
+ attr_accessor :stale_failure_threshold
38
+ attr_accessor :failure_retention_limit
39
+
32
40
  def initialize
33
41
  @service_name = "upright"
34
42
  @user_agent = "Upright/1.0"
@@ -41,11 +49,17 @@ class Upright::Configuration
41
49
  @probes_path = nil
42
50
  @authenticators_path = nil
43
51
 
52
+ @probe_types = Upright::ProbeTypeRegistry.new
53
+
44
54
  @playwright_server_url = ENV["PLAYWRIGHT_SERVER_URL"]
45
55
  @otel_endpoint = ENV["OTEL_EXPORTER_OTLP_ENDPOINT"]
46
56
 
47
57
  @auth_provider = :static_credentials
48
58
  @auth_options = {}
59
+
60
+ @stale_success_threshold = 24.hours
61
+ @stale_failure_threshold = 30.days
62
+ @failure_retention_limit = 20_000
49
63
  end
50
64
 
51
65
  def global_subdomain
@@ -56,6 +56,13 @@ class Upright::Engine < ::Rails::Engine
56
56
  end
57
57
  end
58
58
 
59
+ initializer "upright.probe_types", before: :load_config_initializers do
60
+ Upright.config.probe_types.register :http, name: "HTTP", icon: "🌐"
61
+ Upright.config.probe_types.register :playwright, name: "Playwright", icon: "🎭"
62
+ Upright.config.probe_types.register :smtp, name: "SMTP", icon: "✉️"
63
+ Upright.config.probe_types.register :traceroute, name: "Traceroute", icon: "🛤️"
64
+ end
65
+
59
66
  initializer "upright.frozen_record" do
60
67
  FrozenRecord::Base.base_path = Upright.configuration.frozen_record_path
61
68
  end
@@ -44,12 +44,12 @@ 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",
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Upright
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/upright.rb CHANGED
@@ -19,6 +19,7 @@ require "yabeda/puma/plugin"
19
19
 
20
20
  require "upright/version"
21
21
  require "upright/configuration"
22
+ require "upright/probe_type_registry"
22
23
  require "upright/geohash"
23
24
  require "upright/site"
24
25
  require "upright/metrics"
@@ -38,6 +39,10 @@ module Upright
38
39
  yield(configuration)
39
40
  end
40
41
 
42
+ def probe_types
43
+ configuration.probe_types
44
+ end
45
+
41
46
  def sites
42
47
  @sites ||= load_sites
43
48
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upright
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lewis Buckley
@@ -404,6 +404,7 @@ files:
404
404
  - app/models/concerns/upright/playwright/logging.rb
405
405
  - app/models/concerns/upright/playwright/otel_tracing.rb
406
406
  - app/models/concerns/upright/playwright/video_recording.rb
407
+ - app/models/concerns/upright/probe_result/stale_cleanup.rb
407
408
  - app/models/concerns/upright/probe_yaml_source.rb
408
409
  - app/models/concerns/upright/probeable.rb
409
410
  - app/models/concerns/upright/staggerable.rb
@@ -464,6 +465,7 @@ files:
464
465
  - lib/generators/upright/install/templates/otel_collector.yml
465
466
  - lib/generators/upright/install/templates/prometheus.yml
466
467
  - lib/generators/upright/install/templates/puma.rb
468
+ - lib/generators/upright/install/templates/recurring.yml
467
469
  - lib/generators/upright/install/templates/sites.yml
468
470
  - lib/generators/upright/install/templates/smtp_probes.yml
469
471
  - lib/generators/upright/install/templates/traceroute_probes.yml
@@ -480,6 +482,7 @@ files:
480
482
  - lib/upright/geohash.rb
481
483
  - lib/upright/metrics.rb
482
484
  - lib/upright/playwright/collect_performance_metrics.js
485
+ - lib/upright/probe_type_registry.rb
483
486
  - lib/upright/site.rb
484
487
  - lib/upright/tracing.rb
485
488
  - lib/upright/version.rb
@@ -504,7 +507,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
504
507
  - !ruby/object:Gem::Version
505
508
  version: '0'
506
509
  requirements: []
507
- rubygems_version: 4.0.3
510
+ rubygems_version: 4.0.6
508
511
  specification_version: 4
509
512
  summary: Synthetic monitoring engine with Playwright and Prometheus metrics
510
513
  test_files: []