upright 0.1.2 → 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.
- checksums.yaml +4 -4
- data/LICENSE.md +2 -3
- data/README.md +166 -60
- data/app/assets/stylesheets/upright/dashboard.css +65 -193
- data/app/assets/stylesheets/upright/tables.css +60 -0
- data/app/assets/stylesheets/upright/uptime-bars.css +13 -50
- data/app/controllers/upright/alertmanager_proxy_controller.rb +1 -1
- data/app/controllers/upright/dashboards/probe_statuses_controller.rb +7 -0
- data/app/controllers/upright/probe_results_controller.rb +1 -0
- data/app/controllers/upright/sessions_controller.rb +1 -0
- data/app/helpers/upright/application_helper.rb +10 -0
- data/app/helpers/upright/dashboards_helper.rb +12 -0
- data/app/helpers/upright/probe_results_helper.rb +3 -10
- data/app/javascript/upright/controllers/auto_refresh_controller.js +16 -0
- data/app/models/concerns/upright/playwright/form_authentication.rb +0 -3
- data/app/models/concerns/upright/playwright/lifecycle.rb +3 -2
- data/app/models/concerns/upright/probe_result/stale_cleanup.rb +23 -0
- data/app/models/concerns/upright/probeable.rb +7 -1
- data/app/models/upright/http/request.rb +1 -1
- data/app/models/upright/playwright/storage_state.rb +12 -3
- data/app/models/upright/probe_result.rb +6 -1
- data/app/models/upright/probes/http_probe.rb +6 -2
- data/app/models/upright/probes/status/probe.rb +24 -0
- data/app/models/upright/probes/status/site_status.rb +71 -0
- data/app/models/upright/probes/status.rb +54 -0
- data/app/models/upright/probes/uptime.rb +4 -4
- data/app/models/upright/traceroute/ip_metadata_lookup.rb +1 -2
- data/app/views/layouts/upright/_header.html.erb +2 -1
- data/app/views/upright/dashboards/_uptime_bars.html.erb +2 -2
- data/app/views/upright/dashboards/_uptime_probe_row.html.erb +7 -5
- data/app/views/upright/dashboards/probe_statuses/_matrix.html.erb +48 -0
- data/app/views/upright/dashboards/probe_statuses/show.html.erb +17 -0
- data/app/views/upright/dashboards/uptimes/show.html.erb +1 -1
- data/app/views/upright/probe_results/index.html.erb +7 -4
- data/app/views/upright/sites/index.html.erb +1 -1
- data/config/ci.rb +2 -0
- data/config/credentials/development.key +1 -0
- data/config/credentials/test.key +1 -0
- data/config/routes.rb +1 -0
- data/lib/generators/upright/install/install_generator.rb +52 -2
- data/lib/generators/upright/install/templates/http_probes.yml +1 -0
- data/lib/generators/upright/install/templates/recurring.yml +25 -0
- data/lib/generators/upright/install/templates/smtp_probes.yml +3 -0
- data/lib/generators/upright/install/templates/traceroute_probes.yml +12 -0
- data/lib/generators/upright/install/templates/upright.rb +4 -1
- data/lib/generators/upright/install/templates/upright.rules.yml +5 -5
- data/lib/upright/configuration.rb +14 -0
- data/lib/upright/engine.rb +7 -0
- data/lib/upright/geohash.rb +46 -0
- data/lib/upright/metrics.rb +2 -2
- data/lib/upright/probe_type_registry.rb +33 -0
- data/lib/upright/site.rb +1 -3
- data/lib/upright/version.rb +1 -1
- data/lib/upright.rb +6 -1
- metadata +17 -17
|
@@ -60,4 +60,64 @@
|
|
|
60
60
|
padding: calc(var(--block-space) * 0.4) calc(var(--block-space) * 0.4);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/* Div-based data tables (shared by dashboard views) */
|
|
65
|
+
.data-table {
|
|
66
|
+
background: oklch(var(--lch-ink-lightest) / 85%);
|
|
67
|
+
border-radius: 0.25rem;
|
|
68
|
+
box-shadow: var(--shadow);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.data-table__header {
|
|
72
|
+
background: var(--color-ink-lighter);
|
|
73
|
+
font-size: var(--text-x-small);
|
|
74
|
+
font-weight: 500;
|
|
75
|
+
letter-spacing: 0.08em;
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.data-table__row {
|
|
80
|
+
border-top: 1px solid var(--color-ink-lighter);
|
|
81
|
+
transition: background-color 100ms;
|
|
82
|
+
|
|
83
|
+
&:hover {
|
|
84
|
+
background-color: var(--color-canvas);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.data-table__probe {
|
|
89
|
+
align-items: center;
|
|
90
|
+
color: inherit;
|
|
91
|
+
display: flex;
|
|
92
|
+
gap: 0.75em;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
text-decoration: none;
|
|
95
|
+
|
|
96
|
+
&:hover {
|
|
97
|
+
color: var(--color-link);
|
|
98
|
+
text-decoration: none;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.data-table__probe-name {
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
text-overflow: ellipsis;
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Utilities */
|
|
109
|
+
.text-center {
|
|
110
|
+
text-align: center;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sticky-left {
|
|
114
|
+
background: inherit;
|
|
115
|
+
left: 0;
|
|
116
|
+
position: sticky;
|
|
117
|
+
z-index: 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.scrollable-x {
|
|
121
|
+
overflow-x: auto;
|
|
122
|
+
}
|
|
63
123
|
}
|
|
@@ -1,56 +1,10 @@
|
|
|
1
1
|
@layer components {
|
|
2
|
-
.uptime-
|
|
3
|
-
display: flex;
|
|
4
|
-
flex-direction: column;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.uptime-bars__header {
|
|
8
|
-
background: var(--color-ink-lighter);
|
|
9
|
-
display: grid;
|
|
10
|
-
font-size: var(--text-x-small);
|
|
11
|
-
font-weight: 500;
|
|
12
|
-
gap: var(--block-space);
|
|
13
|
-
grid-template-columns: minmax(200px, 1fr) 1fr auto;
|
|
14
|
-
letter-spacing: 0.04em;
|
|
15
|
-
padding: calc(var(--block-space) * 0.75) var(--block-space);
|
|
16
|
-
text-transform: uppercase;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
.uptime-bars__header,
|
|
20
3
|
.uptime-bars__row {
|
|
21
|
-
border-top: 1px solid var(--color-ink-lighter);
|
|
22
4
|
display: grid;
|
|
23
5
|
gap: var(--block-space);
|
|
24
6
|
grid-template-columns: minmax(200px, 1fr) 1fr auto;
|
|
25
7
|
padding: calc(var(--block-space) * 0.75) var(--block-space);
|
|
26
|
-
transition: background-color 100ms;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.uptime-bars__row:hover {
|
|
30
|
-
background: var(--color-canvas);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.uptime-bars__probe {
|
|
34
|
-
align-items: center;
|
|
35
|
-
color: inherit;
|
|
36
|
-
display: flex;
|
|
37
|
-
gap: 0.75em;
|
|
38
|
-
min-width: 0;
|
|
39
|
-
|
|
40
|
-
&:hover {
|
|
41
|
-
color: var(--color-link);
|
|
42
|
-
text-decoration: none;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.uptime-bars__probe .probe__badge {
|
|
47
|
-
flex-shrink: 0;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.uptime-bars__probe .probe__name {
|
|
51
|
-
overflow: hidden;
|
|
52
|
-
text-overflow: ellipsis;
|
|
53
|
-
white-space: nowrap;
|
|
54
8
|
}
|
|
55
9
|
|
|
56
10
|
.uptime-bars__days {
|
|
@@ -83,10 +37,9 @@
|
|
|
83
37
|
color: var(--color-negative);
|
|
84
38
|
}
|
|
85
39
|
|
|
86
|
-
/* Individual uptime bar
|
|
40
|
+
/* Individual uptime bar */
|
|
87
41
|
.uptime-bar {
|
|
88
42
|
border-radius: 2px;
|
|
89
|
-
cursor: default;
|
|
90
43
|
flex: 1;
|
|
91
44
|
height: 24px;
|
|
92
45
|
max-width: 12px;
|
|
@@ -94,6 +47,16 @@
|
|
|
94
47
|
transition: transform 100ms, filter 100ms;
|
|
95
48
|
}
|
|
96
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
|
+
|
|
97
60
|
.uptime-bar:hover {
|
|
98
61
|
filter: brightness(1.1);
|
|
99
62
|
transform: scaleY(1.15);
|
|
@@ -131,7 +94,7 @@
|
|
|
131
94
|
gap: calc(var(--block-space) * 0.5);
|
|
132
95
|
}
|
|
133
96
|
|
|
134
|
-
.
|
|
97
|
+
.data-table__probe .probe__badge {
|
|
135
98
|
display: none;
|
|
136
99
|
}
|
|
137
100
|
}
|
|
@@ -5,7 +5,7 @@ class Upright::AlertmanagerProxyController < Upright::ApplicationController
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def proxy
|
|
8
|
-
proxy_to_alertmanager request.fullpath.delete_prefix("/alertmanager")
|
|
8
|
+
proxy_to_alertmanager request.fullpath.delete_prefix("/alertmanager"), body: request.body&.read
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
private
|
|
@@ -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
|
|
@@ -3,9 +3,19 @@ module Upright::ApplicationHelper
|
|
|
3
3
|
Upright::Current.site || Upright.sites.first
|
|
4
4
|
end
|
|
5
5
|
|
|
6
|
+
def site_name(site)
|
|
7
|
+
"#{country_flag(site.country)} #{site.city}"
|
|
8
|
+
end
|
|
9
|
+
|
|
6
10
|
def upright_stylesheet_link_tag(**options)
|
|
7
11
|
Upright::Engine.root.join("app/assets/stylesheets/upright").glob("*.css")
|
|
8
12
|
.map { |f| "upright/#{f.basename('.css')}" }.sort
|
|
9
13
|
.then { |stylesheets| stylesheet_link_tag(*stylesheets, **options) }
|
|
10
14
|
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def country_flag(country_code)
|
|
19
|
+
country_code&.upcase&.gsub(/[A-Z]/) { |c| (c.ord + 0x1F1A5).chr(Encoding::UTF_8) }
|
|
20
|
+
end
|
|
11
21
|
end
|
|
@@ -28,4 +28,16 @@ module Upright::DashboardsHelper
|
|
|
28
28
|
mins.zero? ? "#{hours}h" : "#{hours}h #{mins}m"
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
def probe_status_css_class(status)
|
|
33
|
+
if status.nil?
|
|
34
|
+
"probe-status-cell--unknown"
|
|
35
|
+
elsif status.stale?
|
|
36
|
+
"probe-status-cell--stale"
|
|
37
|
+
elsif status.up?
|
|
38
|
+
"probe-status-cell--up"
|
|
39
|
+
else
|
|
40
|
+
"probe-status-cell--down"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
31
43
|
end
|
|
@@ -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
|
-
|
|
12
|
-
content_tag(:span, icon, title:
|
|
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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = { interval: { type: Number, default: 60000 } }
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.timer = setInterval(() => {
|
|
8
|
+
this.element.src = window.location.href
|
|
9
|
+
this.element.reload()
|
|
10
|
+
}, this.intervalValue)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
disconnect() {
|
|
14
|
+
clearInterval(this.timer)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -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
|
|
@@ -33,8 +33,9 @@ module Upright::Playwright::Lifecycle
|
|
|
33
33
|
run_callbacks :page_ready
|
|
34
34
|
yield
|
|
35
35
|
ensure
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# Rescue each step independently so a failed close doesn't prevent video capture
|
|
37
|
+
page&.close rescue Rails.error.report($!)
|
|
38
|
+
context&.close rescue Rails.error.report($!)
|
|
38
39
|
run_callbacks :page_close
|
|
39
40
|
end
|
|
40
41
|
|
|
@@ -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
|
-
|
|
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
|
|
@@ -8,12 +8,16 @@ class Upright::Playwright::StorageState
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def load
|
|
11
|
-
|
|
11
|
+
if exists?
|
|
12
|
+
decrypted_json = encryptor.decrypt_and_verify(path.read)
|
|
13
|
+
JSON.parse(decrypted_json)
|
|
14
|
+
end
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def save(state)
|
|
15
18
|
FileUtils.mkdir_p(storage_dir)
|
|
16
|
-
|
|
19
|
+
encrypted_data = encryptor.encrypt_and_sign(JSON.generate(state))
|
|
20
|
+
path.write(encrypted_data)
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
def clear
|
|
@@ -26,6 +30,11 @@ class Upright::Playwright::StorageState
|
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
def path
|
|
29
|
-
storage_dir.join("#{@service}.
|
|
33
|
+
storage_dir.join("#{@service}.enc")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def encryptor
|
|
37
|
+
key = Rails.application.key_generator.generate_key("playwright_storage_state", 32)
|
|
38
|
+
ActiveSupport::MessageEncryptor.new(key)
|
|
30
39
|
end
|
|
31
40
|
end
|
|
@@ -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)
|
|
@@ -74,11 +74,15 @@ class Upright::Probes::HTTPProbe < FrozenRecord::Base
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def proxy_credentials
|
|
77
|
-
if
|
|
78
|
-
Rails.application.credentials.dig(:proxies,
|
|
77
|
+
if selected_proxy
|
|
78
|
+
Rails.application.credentials.dig(:proxies, selected_proxy.to_sym)
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def selected_proxy
|
|
83
|
+
Array(try(:proxies) || try(:proxy)).sample
|
|
84
|
+
end
|
|
85
|
+
|
|
82
86
|
def record_response_status
|
|
83
87
|
if last_response && !last_response.network_error? && defined?(Yabeda)
|
|
84
88
|
Yabeda.upright_http_response_status.set(
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Upright::Probes::Status::Probe
|
|
2
|
+
include Comparable
|
|
3
|
+
|
|
4
|
+
attr_reader :name, :type, :probe_target, :site_statuses
|
|
5
|
+
|
|
6
|
+
def initialize(name:, type:, probe_target:, site_statuses:)
|
|
7
|
+
@name = name
|
|
8
|
+
@type = type
|
|
9
|
+
@probe_target = probe_target
|
|
10
|
+
@site_statuses = site_statuses
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def status_for_site(code)
|
|
14
|
+
site_statuses.find { |s| s.site_code == code.to_s }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def any_down?
|
|
18
|
+
site_statuses.any?(&:down?)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def <=>(other)
|
|
22
|
+
[ any_down? ? 0 : 1, type, name ] <=> [ other.any_down? ? 0 : 1, other.type, other.name ]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
class Upright::Probes::Status::SiteStatus
|
|
2
|
+
STALE_THRESHOLD = 5.minutes
|
|
3
|
+
|
|
4
|
+
attr_reader :site_code, :site_city
|
|
5
|
+
|
|
6
|
+
def initialize(site_code:, site_city:, values:)
|
|
7
|
+
@site_code = site_code
|
|
8
|
+
@site_city = site_city
|
|
9
|
+
@values = values
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def up?
|
|
13
|
+
latest_value == 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def down?
|
|
17
|
+
!up?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stale?
|
|
21
|
+
if @values.empty?
|
|
22
|
+
true
|
|
23
|
+
else
|
|
24
|
+
Time.at(latest_timestamp) < STALE_THRESHOLD.ago
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def down_since
|
|
29
|
+
if down? && @values.any?
|
|
30
|
+
Time.at(down_start_timestamp)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def down_since_known?
|
|
35
|
+
if down? && @values.any?
|
|
36
|
+
down_start_timestamp != sorted_values.first.first
|
|
37
|
+
else
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
def sorted_values
|
|
44
|
+
@sorted_values ||= @values.sort_by(&:first)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def down_start_timestamp
|
|
48
|
+
@down_start_timestamp ||= begin
|
|
49
|
+
result = sorted_values.last.first
|
|
50
|
+
|
|
51
|
+
sorted_values.reverse_each do |timestamp, value|
|
|
52
|
+
break if value.to_f == 1
|
|
53
|
+
result = timestamp
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def latest_value
|
|
61
|
+
if @values.any?
|
|
62
|
+
sorted_values.last.last.to_f
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def latest_timestamp
|
|
67
|
+
if @values.any?
|
|
68
|
+
sorted_values.last.first
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
class Upright::Probes::Status
|
|
2
|
+
class << self
|
|
3
|
+
def for_type(probe_type)
|
|
4
|
+
results = prometheus_client.query_range(
|
|
5
|
+
query: query(probe_type),
|
|
6
|
+
start: 30.minutes.ago.iso8601,
|
|
7
|
+
end: Time.current.iso8601,
|
|
8
|
+
step: "30s"
|
|
9
|
+
).deep_symbolize_keys
|
|
10
|
+
|
|
11
|
+
build_probes(results[:result])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
def query(probe_type)
|
|
16
|
+
"upright_probe_up#{label_selector(probe_type)}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def label_selector(probe_type)
|
|
20
|
+
matchers = [ "alert_severity!=\"\"" ]
|
|
21
|
+
matchers << "type=\"#{probe_type}\"" if probe_type.present?
|
|
22
|
+
"{#{matchers.join(",")}}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def prometheus_client
|
|
26
|
+
Prometheus::ApiClient.client(
|
|
27
|
+
url: ENV.fetch("PROMETHEUS_URL", "http://localhost:9090"),
|
|
28
|
+
options: { timeout: 30.seconds }
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_probes(results)
|
|
33
|
+
# Group results by probe identity (name + type + probe_target)
|
|
34
|
+
grouped = results.group_by { |r| [ r[:metric][:name], r[:metric][:type], r[:metric][:probe_target] ] }
|
|
35
|
+
|
|
36
|
+
grouped.map do |(_name, _type, _target), series|
|
|
37
|
+
site_statuses = series.map do |s|
|
|
38
|
+
SiteStatus.new(
|
|
39
|
+
site_code: s[:metric][:site_code],
|
|
40
|
+
site_city: s[:metric][:site_city],
|
|
41
|
+
values: s[:values]
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Probe.new(
|
|
46
|
+
name: _name,
|
|
47
|
+
type: _type,
|
|
48
|
+
probe_target: _target,
|
|
49
|
+
site_statuses: site_statuses
|
|
50
|
+
)
|
|
51
|
+
end.sort
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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,7 +1,6 @@
|
|
|
1
1
|
require "net/http"
|
|
2
2
|
require "json"
|
|
3
3
|
require "resolv"
|
|
4
|
-
require "geohash_ruby"
|
|
5
4
|
|
|
6
5
|
class Upright::Traceroute::IpMetadataLookup
|
|
7
6
|
API_URL = "http://ip-api.com/batch"
|
|
@@ -84,7 +83,7 @@ class Upright::Traceroute::IpMetadataLookup
|
|
|
84
83
|
|
|
85
84
|
def encode_geohash(latitude, longitude)
|
|
86
85
|
if latitude && longitude
|
|
87
|
-
Geohash.encode(latitude, longitude, GEOHASH_PRECISION)
|
|
86
|
+
Upright::Geohash.encode(latitude, longitude, GEOHASH_PRECISION)
|
|
88
87
|
end
|
|
89
88
|
end
|
|
90
89
|
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
<div class="header__left">
|
|
3
3
|
<%= link_to "Upright", root_url(subdomain: Upright.configuration.global_subdomain), class: "header__logo" %>
|
|
4
4
|
<% if Upright::Current.site.present? %>
|
|
5
|
-
<span class="header__site"><%= Upright::Current.site
|
|
5
|
+
<span class="header__site"><%= site_name(Upright::Current.site) %></span>
|
|
6
6
|
<% end %>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
9
|
<nav class="header__nav">
|
|
10
10
|
<%= link_to "Probes", upright.site_root_url(subdomain: current_or_default_site.code), class: "header__link" %>
|
|
11
11
|
<%= link_to "Uptime", upright.dashboards_uptime_url(subdomain: Upright.configuration.global_subdomain), class: "header__link" %>
|
|
12
|
+
<%= link_to "Status", upright.dashboards_probe_status_url(subdomain: Upright.configuration.global_subdomain), class: "header__link" %>
|
|
12
13
|
<span class="header__divider"></span>
|
|
13
14
|
<%= link_to "Jobs", upright.jobs_url(subdomain: current_or_default_site.code), class: "header__link" %>
|
|
14
15
|
<span class="header__divider"></span>
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
No probe data available for the selected filters.
|
|
4
4
|
</div>
|
|
5
5
|
<% else %>
|
|
6
|
-
<div class="
|
|
7
|
-
<div class="uptime-bars__header">
|
|
6
|
+
<div class="data-table">
|
|
7
|
+
<div class="data-table__header uptime-bars__header">
|
|
8
8
|
<div>Probe</div>
|
|
9
9
|
<div>Past 30 days</div>
|
|
10
10
|
<div>Uptime</div>
|