upright 0.1.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 +7 -0
- data/LICENSE.md +10 -0
- data/README.md +455 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/upright/_global.css +104 -0
- data/app/assets/stylesheets/upright/artifact.css +148 -0
- data/app/assets/stylesheets/upright/base.css +68 -0
- data/app/assets/stylesheets/upright/buttons.css +21 -0
- data/app/assets/stylesheets/upright/dashboard.css +287 -0
- data/app/assets/stylesheets/upright/forms.css +104 -0
- data/app/assets/stylesheets/upright/header.css +124 -0
- data/app/assets/stylesheets/upright/layout.css +100 -0
- data/app/assets/stylesheets/upright/map.css +25 -0
- data/app/assets/stylesheets/upright/pagination.css +45 -0
- data/app/assets/stylesheets/upright/probes.css +72 -0
- data/app/assets/stylesheets/upright/reset.css +26 -0
- data/app/assets/stylesheets/upright/tables.css +63 -0
- data/app/assets/stylesheets/upright/typography.css +27 -0
- data/app/assets/stylesheets/upright/uptime-bars.css +154 -0
- data/app/controllers/concerns/upright/authentication.rb +21 -0
- data/app/controllers/concerns/upright/subdomain_scoping.rb +18 -0
- data/app/controllers/upright/alertmanager_proxy_controller.rb +21 -0
- data/app/controllers/upright/application_controller.rb +12 -0
- data/app/controllers/upright/artifacts_controller.rb +5 -0
- data/app/controllers/upright/dashboards/uptimes_controller.rb +6 -0
- data/app/controllers/upright/jobs_controller.rb +4 -0
- data/app/controllers/upright/probe_results_controller.rb +17 -0
- data/app/controllers/upright/prometheus_proxy_controller.rb +62 -0
- data/app/controllers/upright/sessions_controller.rb +29 -0
- data/app/controllers/upright/sites_controller.rb +5 -0
- data/app/helpers/upright/application_helper.rb +11 -0
- data/app/helpers/upright/dashboards_helper.rb +31 -0
- data/app/helpers/upright/probe_results_helper.rb +49 -0
- data/app/javascript/upright/application.js +2 -0
- data/app/javascript/upright/controllers/application.js +5 -0
- data/app/javascript/upright/controllers/form_controller.js +7 -0
- data/app/javascript/upright/controllers/index.js +4 -0
- data/app/javascript/upright/controllers/popover_controller.js +15 -0
- data/app/javascript/upright/controllers/probe_results_chart_controller.js +79 -0
- data/app/javascript/upright/controllers/results_table_controller.js +16 -0
- data/app/javascript/upright/controllers/sites_map_controller.js +33 -0
- data/app/jobs/upright/application_job.rb +2 -0
- data/app/jobs/upright/probe_check_job.rb +42 -0
- data/app/models/concerns/upright/exception_recording.rb +38 -0
- data/app/models/concerns/upright/playwright/form_authentication.rb +27 -0
- data/app/models/concerns/upright/playwright/helpers.rb +7 -0
- data/app/models/concerns/upright/playwright/lifecycle.rb +44 -0
- data/app/models/concerns/upright/playwright/logging.rb +87 -0
- data/app/models/concerns/upright/playwright/otel_tracing.rb +137 -0
- data/app/models/concerns/upright/playwright/video_recording.rb +60 -0
- data/app/models/concerns/upright/probe_yaml_source.rb +10 -0
- data/app/models/concerns/upright/probeable.rb +125 -0
- data/app/models/concerns/upright/staggerable.rb +22 -0
- data/app/models/concerns/upright/traceroute/otel_tracing.rb +108 -0
- data/app/models/upright/application_record.rb +3 -0
- data/app/models/upright/artifact.rb +61 -0
- data/app/models/upright/current.rb +9 -0
- data/app/models/upright/http/request.rb +59 -0
- data/app/models/upright/http/response.rb +55 -0
- data/app/models/upright/playwright/authenticator/base.rb +128 -0
- data/app/models/upright/playwright/storage_state.rb +31 -0
- data/app/models/upright/probe_result.rb +31 -0
- data/app/models/upright/probes/http_probe.rb +102 -0
- data/app/models/upright/probes/playwright/base.rb +48 -0
- data/app/models/upright/probes/smtp_probe.rb +48 -0
- data/app/models/upright/probes/traceroute_probe.rb +32 -0
- data/app/models/upright/probes/uptime/summary.rb +36 -0
- data/app/models/upright/probes/uptime.rb +36 -0
- data/app/models/upright/traceroute/hop.rb +49 -0
- data/app/models/upright/traceroute/ip_metadata_lookup.rb +107 -0
- data/app/models/upright/traceroute/mtr_parser.rb +47 -0
- data/app/models/upright/traceroute/result.rb +57 -0
- data/app/models/upright/user.rb +14 -0
- data/app/views/layouts/upright/_header.html.erb +23 -0
- data/app/views/layouts/upright/application.html.erb +25 -0
- data/app/views/upright/active_storage/attachments/_attachment.html.erb +21 -0
- data/app/views/upright/alertmanager_proxy/show.html.erb +1 -0
- data/app/views/upright/artifacts/show.html.erb +9 -0
- data/app/views/upright/dashboards/_uptime_bars.html.erb +17 -0
- data/app/views/upright/dashboards/_uptime_probe_row.html.erb +22 -0
- data/app/views/upright/dashboards/uptimes/show.html.erb +17 -0
- data/app/views/upright/jobs/show.html.erb +1 -0
- data/app/views/upright/probe_results/_pagination.html.erb +19 -0
- data/app/views/upright/probe_results/index.html.erb +72 -0
- data/app/views/upright/prometheus_proxy/show.html.erb +1 -0
- data/app/views/upright/sessions/new.html.erb +6 -0
- data/app/views/upright/sites/index.html.erb +22 -0
- data/config/brakeman.ignore +39 -0
- data/config/ci.rb +7 -0
- data/config/importmap.rb +18 -0
- data/config/routes.rb +41 -0
- data/db/migrate/20250114000001_create_upright_probe_results.rb +19 -0
- data/lib/generators/upright/install/install_generator.rb +83 -0
- data/lib/generators/upright/install/templates/alertmanager.yml +14 -0
- data/lib/generators/upright/install/templates/deploy.yml +118 -0
- data/lib/generators/upright/install/templates/development_alertmanager.yml +11 -0
- data/lib/generators/upright/install/templates/development_prometheus.yml +12 -0
- data/lib/generators/upright/install/templates/docker-compose.yml +38 -0
- data/lib/generators/upright/install/templates/http_probes.yml +14 -0
- data/lib/generators/upright/install/templates/omniauth.rb +8 -0
- data/lib/generators/upright/install/templates/otel_collector.yml +24 -0
- data/lib/generators/upright/install/templates/prometheus.yml +10 -0
- data/lib/generators/upright/install/templates/puma.rb +40 -0
- data/lib/generators/upright/install/templates/sites.yml +26 -0
- data/lib/generators/upright/install/templates/smtp_probes.yml +9 -0
- data/lib/generators/upright/install/templates/upright.rb +21 -0
- data/lib/generators/upright/install/templates/upright.rules.yml +256 -0
- data/lib/generators/upright/playwright_probe/playwright_probe_generator.rb +30 -0
- data/lib/generators/upright/playwright_probe/templates/authenticator.rb.tt +14 -0
- data/lib/generators/upright/playwright_probe/templates/probe.rb.tt +14 -0
- data/lib/omniauth/strategies/static_credentials.rb +57 -0
- data/lib/tasks/upright_tasks.rake +4 -0
- data/lib/upright/configuration.rb +106 -0
- data/lib/upright/engine.rb +157 -0
- data/lib/upright/metrics.rb +62 -0
- data/lib/upright/playwright/collect_performance_metrics.js +36 -0
- data/lib/upright/site.rb +49 -0
- data/lib/upright/tracing.rb +49 -0
- data/lib/upright/version.rb +3 -0
- data/lib/upright.rb +68 -0
- metadata +513 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
class Upright::Playwright::Authenticator::Base
|
|
2
|
+
include Upright::Playwright::Helpers
|
|
3
|
+
|
|
4
|
+
attr_reader :page
|
|
5
|
+
|
|
6
|
+
def self.authenticate_on(page)
|
|
7
|
+
new.authenticate_on(page)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(browser = nil, context_options = {})
|
|
11
|
+
@browser = browser
|
|
12
|
+
@context_options = context_options
|
|
13
|
+
@storage_state = Upright::Playwright::StorageState.new(service_name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def authenticate_on(page)
|
|
17
|
+
@page = page
|
|
18
|
+
authenticate
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def session_valid?
|
|
23
|
+
wait_for_network_idle(page)
|
|
24
|
+
|
|
25
|
+
if page.url == signin_redirect_url
|
|
26
|
+
true
|
|
27
|
+
else
|
|
28
|
+
page.goto(signin_redirect_url, timeout: 10.seconds.in_ms)
|
|
29
|
+
!page.url.include?(signin_path)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def authenticated_context
|
|
34
|
+
if (cached_state = @storage_state.load)
|
|
35
|
+
context = create_context(cached_state)
|
|
36
|
+
return context if context_has_valid_session?(context)
|
|
37
|
+
context.close
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
perform_authentication
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def signin_redirect_url
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def signin_path
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def service_name
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def authenticate
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def context_has_valid_session?(context)
|
|
64
|
+
page = context.new_page
|
|
65
|
+
page.goto(signin_redirect_url, timeout: 10.seconds.in_ms)
|
|
66
|
+
!page.url.include?(signin_path)
|
|
67
|
+
rescue ::Playwright::TimeoutError
|
|
68
|
+
false
|
|
69
|
+
ensure
|
|
70
|
+
page&.close
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def perform_authentication
|
|
74
|
+
context = create_context
|
|
75
|
+
@page = context.new_page
|
|
76
|
+
setup_page_logging(page)
|
|
77
|
+
|
|
78
|
+
authenticate
|
|
79
|
+
|
|
80
|
+
state = context.storage_state
|
|
81
|
+
@storage_state.save(state)
|
|
82
|
+
context.close
|
|
83
|
+
|
|
84
|
+
create_context(state)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def user_agent
|
|
88
|
+
Upright.configuration.user_agent.presence ||
|
|
89
|
+
Upright::Playwright::Lifecycle::DEFAULT_USER_AGENT
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_context(state = nil)
|
|
93
|
+
options = { userAgent: user_agent, serviceWorkers: "block" }
|
|
94
|
+
options[:storageState] = state if state
|
|
95
|
+
options.merge!(@context_options)
|
|
96
|
+
@browser.new_context(**options)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def setup_page_logging(page)
|
|
100
|
+
if defined?(RailsStructuredLogging::Recorder)
|
|
101
|
+
RailsStructuredLogging::Recorder.instance.messages.tap do |messages|
|
|
102
|
+
page.on("response", ->(response) {
|
|
103
|
+
next if skip_logging?(response)
|
|
104
|
+
RailsStructuredLogging::Recorder.instance.sharing(messages)
|
|
105
|
+
log_response(response)
|
|
106
|
+
})
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
page.on("response", ->(response) {
|
|
110
|
+
next if skip_logging?(response)
|
|
111
|
+
log_response(response)
|
|
112
|
+
})
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def log_response(response)
|
|
117
|
+
headers = response.headers.slice("x-request-id", "x-runtime").compact
|
|
118
|
+
Rails.logger.info "#{response.status} #{response.request.resource_type.upcase} #{response.url} #{headers.to_query}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def skip_logging?(response)
|
|
122
|
+
%w[image asset avatar].any? { |pattern| response.url.include?(pattern) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def credentials
|
|
126
|
+
Rails.application.credentials
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Upright::Playwright::StorageState
|
|
2
|
+
def initialize(service)
|
|
3
|
+
@service = service
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def exists?
|
|
7
|
+
path.exist?
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def load
|
|
11
|
+
JSON.parse(path.read) if exists?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def save(state)
|
|
15
|
+
FileUtils.mkdir_p(storage_dir)
|
|
16
|
+
path.write(JSON.pretty_generate(state))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def clear
|
|
20
|
+
path.delete if exists?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
def storage_dir
|
|
25
|
+
Upright.configuration.storage_state_dir
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def path
|
|
29
|
+
storage_dir.join("#{@service}.json")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Upright::ProbeResult < Upright::ApplicationRecord
|
|
2
|
+
include Upright::ExceptionRecording
|
|
3
|
+
|
|
4
|
+
has_many_attached :artifacts
|
|
5
|
+
|
|
6
|
+
scope :by_type, ->(type) { where(probe_type: type) if type.present? }
|
|
7
|
+
scope :by_status, ->(status) { where(status: status) if status.present? }
|
|
8
|
+
scope :by_name, ->(name) { where(probe_name: name) if name.present? }
|
|
9
|
+
scope :stale, -> { where(created_at: ...24.hours.ago) }
|
|
10
|
+
|
|
11
|
+
enum :status, [ :ok, :fail ]
|
|
12
|
+
|
|
13
|
+
after_create :increment_metrics
|
|
14
|
+
|
|
15
|
+
def to_chart
|
|
16
|
+
{
|
|
17
|
+
probe_name: probe_name,
|
|
18
|
+
status: status,
|
|
19
|
+
created_at: created_at.iso8601,
|
|
20
|
+
duration: duration.to_f
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
def increment_metrics
|
|
26
|
+
labels = { type: probe_type, name: probe_name, probe_target: probe_target, probe_service: probe_service }
|
|
27
|
+
|
|
28
|
+
Yabeda.upright_probe_duration_seconds.set(labels.merge(status: status), duration.to_f)
|
|
29
|
+
Yabeda.upright_probe_up.set(labels, ok? ? 1 : 0)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
class Upright::Probes::HTTPProbe < FrozenRecord::Base
|
|
2
|
+
include Upright::Probeable
|
|
3
|
+
include Upright::ProbeYamlSource
|
|
4
|
+
|
|
5
|
+
stagger_by_site 3.seconds
|
|
6
|
+
|
|
7
|
+
DEFAULT_EXPECTED_STATUS = 200..399
|
|
8
|
+
|
|
9
|
+
attr_accessor :last_response
|
|
10
|
+
|
|
11
|
+
def check
|
|
12
|
+
self.last_response = perform_request
|
|
13
|
+
record_response_status
|
|
14
|
+
status_matches_expected?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def on_check_recorded(probe_result)
|
|
18
|
+
attach_verbose_log(probe_result)
|
|
19
|
+
attach_response_body(probe_result)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def probe_type = "http"
|
|
23
|
+
def probe_target = url
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
def perform_request
|
|
27
|
+
Upright::HTTP::Request.new(url, **request_options).get
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def request_options
|
|
31
|
+
credentials_hash.merge(proxy_hash)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def status_matches_expected?
|
|
35
|
+
if last_response.network_error?
|
|
36
|
+
false
|
|
37
|
+
else
|
|
38
|
+
last_response.status_in?(expected_status_range)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def expected_status_range
|
|
43
|
+
if try(:expected_status).is_a?(Integer)
|
|
44
|
+
expected_status..expected_status
|
|
45
|
+
else
|
|
46
|
+
DEFAULT_EXPECTED_STATUS
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def credentials_hash
|
|
51
|
+
if credentials
|
|
52
|
+
{ username: credentials[:username], password: credentials[:password] }
|
|
53
|
+
else
|
|
54
|
+
{}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def credentials
|
|
59
|
+
if try(:basic_auth_credentials)
|
|
60
|
+
Rails.application.credentials.dig(:http_probes, basic_auth_credentials.to_sym)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def proxy_hash
|
|
65
|
+
if proxy_credentials
|
|
66
|
+
{
|
|
67
|
+
proxy: proxy_credentials[:url],
|
|
68
|
+
proxy_username: proxy_credentials[:username],
|
|
69
|
+
proxy_password: proxy_credentials[:password]
|
|
70
|
+
}.compact
|
|
71
|
+
else
|
|
72
|
+
{}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def proxy_credentials
|
|
77
|
+
if try(:proxy)
|
|
78
|
+
Rails.application.credentials.dig(:proxies, proxy.to_sym)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def record_response_status
|
|
83
|
+
if last_response && !last_response.network_error? && defined?(Yabeda)
|
|
84
|
+
Yabeda.upright_http_response_status.set(
|
|
85
|
+
{ name: probe_name, probe_target: probe_target, probe_service: probe_service },
|
|
86
|
+
last_response.status
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def attach_verbose_log(probe_result)
|
|
92
|
+
if last_response
|
|
93
|
+
Upright::Artifact.new(name: "curl.log", content: last_response.verbose_log_content).attach_to(probe_result)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def attach_response_body(probe_result)
|
|
98
|
+
if last_response && last_response.body.present?
|
|
99
|
+
Upright::Artifact.new(name: "response.#{last_response.file_extension}", content: last_response.body).attach_to(probe_result)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "playwright"
|
|
2
|
+
|
|
3
|
+
class Upright::Probes::Playwright::Base
|
|
4
|
+
include ActiveSupport::Callbacks
|
|
5
|
+
include Upright::Probeable
|
|
6
|
+
|
|
7
|
+
define_callbacks :perform_check
|
|
8
|
+
|
|
9
|
+
include Upright::Playwright::Lifecycle
|
|
10
|
+
include Upright::Playwright::FormAuthentication
|
|
11
|
+
include Upright::Playwright::Logging
|
|
12
|
+
include Upright::Playwright::OtelTracing
|
|
13
|
+
include Upright::Playwright::VideoRecording
|
|
14
|
+
include Upright::Playwright::Helpers
|
|
15
|
+
|
|
16
|
+
set_callback :perform_check, :after, :wait_for_network_idle
|
|
17
|
+
|
|
18
|
+
def self.check
|
|
19
|
+
new.perform_check
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.check_and_record_later
|
|
23
|
+
Upright::ProbeCheckJob.set(wait: stagger_delay).perform_later(name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def perform_check
|
|
27
|
+
with_browser do |browser|
|
|
28
|
+
with_context(browser, **video_recording_options) do
|
|
29
|
+
run_callbacks :perform_check do
|
|
30
|
+
check
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_check_recorded(probe_result)
|
|
41
|
+
attach_video(probe_result)
|
|
42
|
+
attach_log(probe_result)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def probe_type = "playwright"
|
|
46
|
+
def probe_target = nil
|
|
47
|
+
def probe_service = authentication_service&.to_s
|
|
48
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "net/smtp"
|
|
2
|
+
|
|
3
|
+
class Upright::Probes::SMTPProbe < FrozenRecord::Base
|
|
4
|
+
include Upright::Probeable
|
|
5
|
+
include Upright::ProbeYamlSource
|
|
6
|
+
|
|
7
|
+
stagger_by_site 3.seconds
|
|
8
|
+
|
|
9
|
+
attr_accessor :smtp_log
|
|
10
|
+
|
|
11
|
+
def check
|
|
12
|
+
self.smtp_log = StringIO.new
|
|
13
|
+
|
|
14
|
+
current_site = Upright.current_site
|
|
15
|
+
|
|
16
|
+
smtp = Net::SMTP.new(host)
|
|
17
|
+
smtp.open_timeout = current_site.default_timeout
|
|
18
|
+
smtp.read_timeout = current_site.default_timeout
|
|
19
|
+
smtp.debug_output = smtp_log
|
|
20
|
+
|
|
21
|
+
smtp.start("upright") { }
|
|
22
|
+
|
|
23
|
+
true
|
|
24
|
+
rescue Net::SMTPError, Net::OpenTimeout, Net::ReadTimeout
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def on_check_recorded(probe_result)
|
|
29
|
+
attach_log(probe_result)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def probe_type = "smtp"
|
|
33
|
+
def probe_target = host
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
def attach_log(probe_result)
|
|
37
|
+
if smtp_log
|
|
38
|
+
smtp_log.rewind
|
|
39
|
+
log_content = smtp_log.read
|
|
40
|
+
|
|
41
|
+
if log_content.present?
|
|
42
|
+
logger.debug { log_content }
|
|
43
|
+
|
|
44
|
+
Upright::Artifact.new(name: "smtp.log", content: log_content).attach_to(probe_result)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class Upright::Probes::TracerouteProbe < FrozenRecord::Base
|
|
2
|
+
include Upright::Probeable
|
|
3
|
+
include Upright::ProbeYamlSource
|
|
4
|
+
include Upright::Traceroute::OtelTracing
|
|
5
|
+
|
|
6
|
+
stagger_by_site 3.seconds
|
|
7
|
+
|
|
8
|
+
TIMEOUT = 60.seconds
|
|
9
|
+
MAX_HOPS = Upright::Traceroute::Result::MAX_HOPS
|
|
10
|
+
PROBE_COUNT = Upright::Traceroute::Result::PROBE_COUNT
|
|
11
|
+
|
|
12
|
+
def check
|
|
13
|
+
Upright::Traceroute::Result.for(host).tap do |result|
|
|
14
|
+
@traceroute_result = result
|
|
15
|
+
trace_result(result)
|
|
16
|
+
end.reached_destination?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_check_recorded(probe_result)
|
|
20
|
+
attach_traceroute_json(probe_result, @traceroute_result)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def probe_type = "traceroute"
|
|
24
|
+
def probe_target = host
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
def attach_traceroute_json(probe_result, traceroute_result)
|
|
28
|
+
if traceroute_result.raw_json.present?
|
|
29
|
+
Upright::Artifact.new(name: "traceroute.json", content: traceroute_result.raw_json).attach_to(probe_result)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class Upright::Probes::Uptime::Summary
|
|
2
|
+
include Comparable
|
|
3
|
+
|
|
4
|
+
def initialize(result)
|
|
5
|
+
@result = result
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def name
|
|
9
|
+
@result[:metric][:name]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def type
|
|
13
|
+
@result[:metric][:type]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def overall_uptime
|
|
17
|
+
if daily_uptimes.empty?
|
|
18
|
+
0
|
|
19
|
+
else
|
|
20
|
+
(daily_uptimes.values.sum / daily_uptimes.size) * 100
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def uptime_for_date(date)
|
|
25
|
+
daily_uptimes[date.to_date]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def <=>(other)
|
|
29
|
+
[ type, name ] <=> [ other.type, other.name ]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
def daily_uptimes
|
|
34
|
+
@daily_uptimes ||= @result[:values].to_h { |timestamp, value| [ Time.at(timestamp).to_date, value.to_f ] }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class Upright::Probes::Uptime
|
|
2
|
+
class << self
|
|
3
|
+
def all
|
|
4
|
+
for_type(nil)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def for_type(probe_type)
|
|
8
|
+
results = prometheus_client.query_range(
|
|
9
|
+
query: query(probe_type),
|
|
10
|
+
start: 30.days.ago.iso8601,
|
|
11
|
+
end: Time.current.iso8601,
|
|
12
|
+
step: "86400s" # 1 day
|
|
13
|
+
).deep_symbolize_keys
|
|
14
|
+
|
|
15
|
+
results[:result].map { |result| Summary.new(result) }.sort
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
def query(probe_type)
|
|
20
|
+
"upright:probe_uptime_daily#{label_selector(probe_type)}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def label_selector(probe_type)
|
|
24
|
+
if probe_type.present?
|
|
25
|
+
"{type=\"#{probe_type}\"}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def prometheus_client
|
|
30
|
+
Prometheus::ApiClient.client(
|
|
31
|
+
url: ENV.fetch("PROMETHEUS_URL", "http://localhost:9090"),
|
|
32
|
+
options: { timeout: 30.seconds }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class Upright::Traceroute::Hop
|
|
2
|
+
attr_reader :number, :ip, :loss_percent,
|
|
3
|
+
:last_ms, :avg_ms, :best_ms, :worst_ms, :stddev_ms,
|
|
4
|
+
:hostname, :as, :isp, :city, :country, :country_code, :geohash
|
|
5
|
+
|
|
6
|
+
def initialize(hop_number: nil, ip: nil, hostname: nil, loss_percent: nil,
|
|
7
|
+
last_ms: nil, avg_ms: nil, best_ms: nil, worst_ms: nil, stddev_ms: nil,
|
|
8
|
+
as: nil, isp: nil, city: nil, country: nil, country_code: nil, geohash: nil)
|
|
9
|
+
@number = hop_number
|
|
10
|
+
@ip = ip
|
|
11
|
+
@hostname = hostname
|
|
12
|
+
@loss_percent = loss_percent
|
|
13
|
+
@last_ms = last_ms
|
|
14
|
+
@avg_ms = avg_ms
|
|
15
|
+
@best_ms = best_ms
|
|
16
|
+
@worst_ms = worst_ms
|
|
17
|
+
@stddev_ms = stddev_ms
|
|
18
|
+
@as = as
|
|
19
|
+
@isp = isp
|
|
20
|
+
@city = city
|
|
21
|
+
@country = country
|
|
22
|
+
@country_code = country_code
|
|
23
|
+
@geohash = geohash
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def responded?
|
|
27
|
+
ip.present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def high_packet_loss?
|
|
31
|
+
loss_percent && loss_percent > 50
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def any_packet_loss?
|
|
35
|
+
loss_percent && loss_percent > 0
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def display_name
|
|
39
|
+
if responded?
|
|
40
|
+
if isp.present?
|
|
41
|
+
isp
|
|
42
|
+
else
|
|
43
|
+
ip
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
"???"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "resolv"
|
|
4
|
+
require "geohash_ruby"
|
|
5
|
+
|
|
6
|
+
class Upright::Traceroute::IpMetadataLookup
|
|
7
|
+
API_URL = "http://ip-api.com/batch"
|
|
8
|
+
TIMEOUT = 5.seconds
|
|
9
|
+
GEOHASH_PRECISION = 6
|
|
10
|
+
CACHE_TTL = 24.hours
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def for_many(ips)
|
|
14
|
+
results, uncached_ips = partition_cached(ips)
|
|
15
|
+
|
|
16
|
+
if uncached_ips.any?
|
|
17
|
+
fetch_batch(uncached_ips).each do |ip, metadata|
|
|
18
|
+
cache_write(ip, metadata)
|
|
19
|
+
results[ip] = metadata
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
results
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def clear_cache
|
|
27
|
+
cache.clear
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
def partition_cached(ips)
|
|
32
|
+
results = {}
|
|
33
|
+
uncached = []
|
|
34
|
+
|
|
35
|
+
valid_ips(ips).each do |ip|
|
|
36
|
+
cached = cache_read(ip)
|
|
37
|
+
if cached
|
|
38
|
+
results[ip] = cached
|
|
39
|
+
else
|
|
40
|
+
uncached << ip
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
[ results, uncached ]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def valid_ips(ips)
|
|
48
|
+
ips.compact.uniq.select { |ip| ip =~ Resolv::IPv4::Regex }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_batch(ips)
|
|
52
|
+
uri = URI(API_URL)
|
|
53
|
+
request = Net::HTTP::Post.new(uri)
|
|
54
|
+
request.content_type = "application/json"
|
|
55
|
+
request.body = ips.to_json
|
|
56
|
+
|
|
57
|
+
response = Net::HTTP.start(uri.hostname, uri.port, read_timeout: TIMEOUT, open_timeout: TIMEOUT) do |http|
|
|
58
|
+
http.request(request)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
62
|
+
parse_response(JSON.parse(response.body))
|
|
63
|
+
else
|
|
64
|
+
{}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_response(results)
|
|
69
|
+
results.select { |result| result["status"] == "success" }.to_h do |result|
|
|
70
|
+
[ result["query"], build_metadata(result) ]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_metadata(result)
|
|
75
|
+
{
|
|
76
|
+
as: result["as"],
|
|
77
|
+
isp: result["isp"],
|
|
78
|
+
city: result["city"],
|
|
79
|
+
country: result["country"],
|
|
80
|
+
country_code: result["countryCode"],
|
|
81
|
+
geohash: encode_geohash(result["lat"], result["lon"])
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def encode_geohash(latitude, longitude)
|
|
86
|
+
if latitude && longitude
|
|
87
|
+
Geohash.encode(latitude, longitude, GEOHASH_PRECISION)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def cache_read(ip)
|
|
92
|
+
cache.read("traceroute/ip_metadata/#{ip}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def cache_write(ip, metadata)
|
|
96
|
+
cache.write("traceroute/ip_metadata/#{ip}", metadata, expires_in: CACHE_TTL)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def cache
|
|
100
|
+
@cache ||= if Rails.env.test?
|
|
101
|
+
ActiveSupport::Cache::MemoryStore.new
|
|
102
|
+
else
|
|
103
|
+
ActiveSupport::Cache::FileStore.new(Rails.root.join("storage/ip_metadata"))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "resolv"
|
|
3
|
+
|
|
4
|
+
class Upright::Traceroute::MtrParser
|
|
5
|
+
def initialize(json_output)
|
|
6
|
+
@json_output = json_output
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def parse
|
|
10
|
+
data = JSON.parse(@json_output)
|
|
11
|
+
hubs = data.dig("report", "hubs").to_a
|
|
12
|
+
|
|
13
|
+
hubs.map { |hub| parse_hub(hub) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
def parse_hub(hub)
|
|
18
|
+
{
|
|
19
|
+
**ip_and_hostname(hub["host"]),
|
|
20
|
+
hop_number: hub["count"],
|
|
21
|
+
loss_percent: hub["Loss%"],
|
|
22
|
+
last_ms: hub["Last"],
|
|
23
|
+
avg_ms: hub["Avg"],
|
|
24
|
+
best_ms: hub["Best"],
|
|
25
|
+
worst_ms: hub["Wrst"],
|
|
26
|
+
stddev_ms: hub["StDev"]
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ip_and_hostname(host)
|
|
31
|
+
if host.nil? || host == "???"
|
|
32
|
+
{ ip: nil, hostname: nil }
|
|
33
|
+
elsif ip_address?(host)
|
|
34
|
+
{ ip: host, hostname: nil }
|
|
35
|
+
else
|
|
36
|
+
{ ip: resolve_ip(host), hostname: host }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve_ip(hostname)
|
|
41
|
+
Resolv.getaddress(hostname) rescue nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ip_address?(address)
|
|
45
|
+
address.match?(Resolv::IPv4::Regex)
|
|
46
|
+
end
|
|
47
|
+
end
|