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.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.md +10 -0
  3. data/README.md +455 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/upright/_global.css +104 -0
  6. data/app/assets/stylesheets/upright/artifact.css +148 -0
  7. data/app/assets/stylesheets/upright/base.css +68 -0
  8. data/app/assets/stylesheets/upright/buttons.css +21 -0
  9. data/app/assets/stylesheets/upright/dashboard.css +287 -0
  10. data/app/assets/stylesheets/upright/forms.css +104 -0
  11. data/app/assets/stylesheets/upright/header.css +124 -0
  12. data/app/assets/stylesheets/upright/layout.css +100 -0
  13. data/app/assets/stylesheets/upright/map.css +25 -0
  14. data/app/assets/stylesheets/upright/pagination.css +45 -0
  15. data/app/assets/stylesheets/upright/probes.css +72 -0
  16. data/app/assets/stylesheets/upright/reset.css +26 -0
  17. data/app/assets/stylesheets/upright/tables.css +63 -0
  18. data/app/assets/stylesheets/upright/typography.css +27 -0
  19. data/app/assets/stylesheets/upright/uptime-bars.css +154 -0
  20. data/app/controllers/concerns/upright/authentication.rb +21 -0
  21. data/app/controllers/concerns/upright/subdomain_scoping.rb +18 -0
  22. data/app/controllers/upright/alertmanager_proxy_controller.rb +21 -0
  23. data/app/controllers/upright/application_controller.rb +12 -0
  24. data/app/controllers/upright/artifacts_controller.rb +5 -0
  25. data/app/controllers/upright/dashboards/uptimes_controller.rb +6 -0
  26. data/app/controllers/upright/jobs_controller.rb +4 -0
  27. data/app/controllers/upright/probe_results_controller.rb +17 -0
  28. data/app/controllers/upright/prometheus_proxy_controller.rb +62 -0
  29. data/app/controllers/upright/sessions_controller.rb +29 -0
  30. data/app/controllers/upright/sites_controller.rb +5 -0
  31. data/app/helpers/upright/application_helper.rb +11 -0
  32. data/app/helpers/upright/dashboards_helper.rb +31 -0
  33. data/app/helpers/upright/probe_results_helper.rb +49 -0
  34. data/app/javascript/upright/application.js +2 -0
  35. data/app/javascript/upright/controllers/application.js +5 -0
  36. data/app/javascript/upright/controllers/form_controller.js +7 -0
  37. data/app/javascript/upright/controllers/index.js +4 -0
  38. data/app/javascript/upright/controllers/popover_controller.js +15 -0
  39. data/app/javascript/upright/controllers/probe_results_chart_controller.js +79 -0
  40. data/app/javascript/upright/controllers/results_table_controller.js +16 -0
  41. data/app/javascript/upright/controllers/sites_map_controller.js +33 -0
  42. data/app/jobs/upright/application_job.rb +2 -0
  43. data/app/jobs/upright/probe_check_job.rb +42 -0
  44. data/app/models/concerns/upright/exception_recording.rb +38 -0
  45. data/app/models/concerns/upright/playwright/form_authentication.rb +27 -0
  46. data/app/models/concerns/upright/playwright/helpers.rb +7 -0
  47. data/app/models/concerns/upright/playwright/lifecycle.rb +44 -0
  48. data/app/models/concerns/upright/playwright/logging.rb +87 -0
  49. data/app/models/concerns/upright/playwright/otel_tracing.rb +137 -0
  50. data/app/models/concerns/upright/playwright/video_recording.rb +60 -0
  51. data/app/models/concerns/upright/probe_yaml_source.rb +10 -0
  52. data/app/models/concerns/upright/probeable.rb +125 -0
  53. data/app/models/concerns/upright/staggerable.rb +22 -0
  54. data/app/models/concerns/upright/traceroute/otel_tracing.rb +108 -0
  55. data/app/models/upright/application_record.rb +3 -0
  56. data/app/models/upright/artifact.rb +61 -0
  57. data/app/models/upright/current.rb +9 -0
  58. data/app/models/upright/http/request.rb +59 -0
  59. data/app/models/upright/http/response.rb +55 -0
  60. data/app/models/upright/playwright/authenticator/base.rb +128 -0
  61. data/app/models/upright/playwright/storage_state.rb +31 -0
  62. data/app/models/upright/probe_result.rb +31 -0
  63. data/app/models/upright/probes/http_probe.rb +102 -0
  64. data/app/models/upright/probes/playwright/base.rb +48 -0
  65. data/app/models/upright/probes/smtp_probe.rb +48 -0
  66. data/app/models/upright/probes/traceroute_probe.rb +32 -0
  67. data/app/models/upright/probes/uptime/summary.rb +36 -0
  68. data/app/models/upright/probes/uptime.rb +36 -0
  69. data/app/models/upright/traceroute/hop.rb +49 -0
  70. data/app/models/upright/traceroute/ip_metadata_lookup.rb +107 -0
  71. data/app/models/upright/traceroute/mtr_parser.rb +47 -0
  72. data/app/models/upright/traceroute/result.rb +57 -0
  73. data/app/models/upright/user.rb +14 -0
  74. data/app/views/layouts/upright/_header.html.erb +23 -0
  75. data/app/views/layouts/upright/application.html.erb +25 -0
  76. data/app/views/upright/active_storage/attachments/_attachment.html.erb +21 -0
  77. data/app/views/upright/alertmanager_proxy/show.html.erb +1 -0
  78. data/app/views/upright/artifacts/show.html.erb +9 -0
  79. data/app/views/upright/dashboards/_uptime_bars.html.erb +17 -0
  80. data/app/views/upright/dashboards/_uptime_probe_row.html.erb +22 -0
  81. data/app/views/upright/dashboards/uptimes/show.html.erb +17 -0
  82. data/app/views/upright/jobs/show.html.erb +1 -0
  83. data/app/views/upright/probe_results/_pagination.html.erb +19 -0
  84. data/app/views/upright/probe_results/index.html.erb +72 -0
  85. data/app/views/upright/prometheus_proxy/show.html.erb +1 -0
  86. data/app/views/upright/sessions/new.html.erb +6 -0
  87. data/app/views/upright/sites/index.html.erb +22 -0
  88. data/config/brakeman.ignore +39 -0
  89. data/config/ci.rb +7 -0
  90. data/config/importmap.rb +18 -0
  91. data/config/routes.rb +41 -0
  92. data/db/migrate/20250114000001_create_upright_probe_results.rb +19 -0
  93. data/lib/generators/upright/install/install_generator.rb +83 -0
  94. data/lib/generators/upright/install/templates/alertmanager.yml +14 -0
  95. data/lib/generators/upright/install/templates/deploy.yml +118 -0
  96. data/lib/generators/upright/install/templates/development_alertmanager.yml +11 -0
  97. data/lib/generators/upright/install/templates/development_prometheus.yml +12 -0
  98. data/lib/generators/upright/install/templates/docker-compose.yml +38 -0
  99. data/lib/generators/upright/install/templates/http_probes.yml +14 -0
  100. data/lib/generators/upright/install/templates/omniauth.rb +8 -0
  101. data/lib/generators/upright/install/templates/otel_collector.yml +24 -0
  102. data/lib/generators/upright/install/templates/prometheus.yml +10 -0
  103. data/lib/generators/upright/install/templates/puma.rb +40 -0
  104. data/lib/generators/upright/install/templates/sites.yml +26 -0
  105. data/lib/generators/upright/install/templates/smtp_probes.yml +9 -0
  106. data/lib/generators/upright/install/templates/upright.rb +21 -0
  107. data/lib/generators/upright/install/templates/upright.rules.yml +256 -0
  108. data/lib/generators/upright/playwright_probe/playwright_probe_generator.rb +30 -0
  109. data/lib/generators/upright/playwright_probe/templates/authenticator.rb.tt +14 -0
  110. data/lib/generators/upright/playwright_probe/templates/probe.rb.tt +14 -0
  111. data/lib/omniauth/strategies/static_credentials.rb +57 -0
  112. data/lib/tasks/upright_tasks.rake +4 -0
  113. data/lib/upright/configuration.rb +106 -0
  114. data/lib/upright/engine.rb +157 -0
  115. data/lib/upright/metrics.rb +62 -0
  116. data/lib/upright/playwright/collect_performance_metrics.js +36 -0
  117. data/lib/upright/site.rb +49 -0
  118. data/lib/upright/tracing.rb +49 -0
  119. data/lib/upright/version.rb +3 -0
  120. data/lib/upright.rb +68 -0
  121. metadata +513 -0
@@ -0,0 +1,137 @@
1
+ module Upright::Playwright::OtelTracing
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ set_callback :perform_check, :around, :with_resource_tracing
6
+ end
7
+
8
+ def with_resource_tracing(&block)
9
+ return block.call unless defined?(OpenTelemetry)
10
+
11
+ collected_responses = []
12
+
13
+ page.on("response", ->(response) {
14
+ unless skip_span?(response)
15
+ collected_responses << response
16
+ end
17
+ })
18
+
19
+ trace_check_with_responses(collected_responses, &block)
20
+ end
21
+
22
+ private
23
+ SKIP_URL_PATTERNS = %w[ image asset avatar ]
24
+
25
+ def tracer
26
+ @tracer ||= OpenTelemetry.tracer_provider.tracer(
27
+ Upright.configuration.service_name,
28
+ Upright::VERSION
29
+ )
30
+ end
31
+
32
+ def skip_span?(response)
33
+ SKIP_URL_PATTERNS.any? { |skip_pattern| response.url.include?(skip_pattern) || response.request.resource_type == skip_pattern }
34
+ end
35
+
36
+ def trace_check_with_responses(collected_responses, &block)
37
+ tracer.in_span(:probe, attributes: root_span_attributes) do |span|
38
+ result = block.call
39
+ create_response_spans_for(collected_responses)
40
+ result
41
+ end
42
+ end
43
+
44
+ def root_span_attributes
45
+ {
46
+ "probe.name" => self.class.name
47
+ }
48
+ end
49
+
50
+ def create_response_spans_for(collected_responses)
51
+ collected_responses.each do |response|
52
+ create_response_span(response)
53
+ end
54
+ end
55
+
56
+ def create_response_span(response)
57
+ span_time_range = calculate_span_timestamps(response.request.timing)
58
+ span_name = "#{response.request.resource_type} #{extract_path(response.url)}"
59
+
60
+ span = tracer.start_span(
61
+ span_name,
62
+ kind: :client,
63
+ start_timestamp: span_time_range.begin,
64
+ attributes: response_span_attributes(response)
65
+ )
66
+
67
+ span.status = map_http_status_to_otel(response.status)
68
+ span.set_attribute("error", true) if response.status >= 400
69
+ add_browser_performance_metrics_to_span(span) if response.request.resource_type == "document"
70
+ span.finish(end_timestamp: span_time_range.end)
71
+ end
72
+
73
+ def calculate_span_timestamps(timing)
74
+ start_timestamp_seconds = timing[:startTime] / 1000.0
75
+ end_timestamp_seconds = (timing[:startTime] + timing[:responseEnd]) / 1000.0
76
+ start_timestamp_seconds..end_timestamp_seconds
77
+ end
78
+
79
+ def extract_path(url)
80
+ uri = URI.parse(url)
81
+ path = uri.path.presence || "/"
82
+ path += "?#{uri.query}" if uri.query.present?
83
+ path
84
+ end
85
+
86
+ def response_span_attributes(response)
87
+ {
88
+ "http.url" => response.url,
89
+ "http.status_code" => response.status,
90
+ "http.resource_type" => response.request.resource_type,
91
+ "http.timing.time_to_first_byte" => calculate_time_to_first_byte(response.request.timing),
92
+ "http.timing.total" => calculate_total_time(response.request.timing),
93
+ "http.request.size" => response.request.sizes[:requestBodySize],
94
+ "http.response.size" => response.request.sizes[:responseBodySize],
95
+ "http.response.header.x_runtime" => response.headers["x-runtime"].to_f,
96
+ "http.response.header.x_request_id" => response.headers["x-request-id"],
97
+ "http.response.header.server_timing" => response.headers["server-timing"]
98
+ }.compact
99
+ end
100
+
101
+ def calculate_time_to_first_byte(timing)
102
+ (timing[:responseStart] - timing[:requestStart]).round(2)
103
+ end
104
+
105
+ def calculate_total_time(timing)
106
+ (timing[:responseEnd] - timing[:requestStart]).round(2)
107
+ end
108
+
109
+ def map_http_status_to_otel(http_status)
110
+ case http_status
111
+ when 200..299
112
+ OpenTelemetry::Trace::Status.ok
113
+ when 400..599
114
+ OpenTelemetry::Trace::Status.error("HTTP #{http_status}")
115
+ else
116
+ OpenTelemetry::Trace::Status.unset
117
+ end
118
+ end
119
+
120
+ def add_browser_performance_metrics_to_span(span)
121
+ metrics = collect_browser_performance_metrics
122
+
123
+ metrics.each do |key, value|
124
+ span.set_attribute("browser.performance.#{key}", value) if value
125
+ end
126
+ end
127
+
128
+ def collect_browser_performance_metrics
129
+ page.evaluate(performance_metrics_script)
130
+ end
131
+
132
+ def performance_metrics_script
133
+ @performance_metrics_script ||= File.read(
134
+ Upright::Engine.root.join("lib", "upright", "playwright", "collect_performance_metrics.js")
135
+ )
136
+ end
137
+ end
@@ -0,0 +1,60 @@
1
+ module Upright::Playwright::VideoRecording
2
+ extend ActiveSupport::Concern
3
+
4
+ VIDEO_SIZE = { width: 1280, height: 720 }
5
+
6
+ included do
7
+ attr_accessor :video_path, :video_object
8
+
9
+ set_callback :page_ready, :after, :capture_video_reference
10
+ set_callback :page_close, :after, :finalize_video
11
+ end
12
+
13
+ private
14
+ def video_dir
15
+ Upright.configuration.video_storage_dir
16
+ end
17
+
18
+ def finalize_video
19
+ save_video
20
+ end
21
+
22
+ def video_recording_options
23
+ if record_video?
24
+ FileUtils.mkdir_p(video_dir)
25
+ { record_video_dir: video_dir.to_s, record_video_size: VIDEO_SIZE }
26
+ else
27
+ {}
28
+ end
29
+ end
30
+
31
+ def record_video?
32
+ !Rails.env.test?
33
+ end
34
+
35
+ def capture_video_reference
36
+ self.video_object = page.video if record_video?
37
+ end
38
+
39
+ def save_video
40
+ if video_object
41
+ self.video_path = video_dir.join("#{SecureRandom.hex}.webm").to_s
42
+ video_object.save_as(video_path)
43
+ end
44
+ end
45
+
46
+ def attach_video(probe_result)
47
+ if video_path
48
+ File.open(video_path, "rb") do |file|
49
+ Upright::Artifact.new(name: "#{probe_name}.webm", content: file).attach_to(probe_result, timestamped: true)
50
+ end
51
+
52
+ if logger.respond_to?(:struct) && probe_result.artifacts.any?
53
+ logger.struct probe_artifact_url: Rails.application.routes.url_helpers.rails_blob_url(probe_result.artifacts.first, expires_in: 24.hours)
54
+ end
55
+
56
+ FileUtils.rm(video_path)
57
+ self.video_path = nil
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,10 @@
1
+ module Upright::ProbeYamlSource
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def file_path
6
+ filename = name.demodulize.underscore.pluralize
7
+ Upright.configuration.probes_path.join("#{filename}.yml").to_s
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,125 @@
1
+ module Upright::Probeable
2
+ extend ActiveSupport::Concern
3
+ include Upright::Staggerable
4
+
5
+ TYPES = %w[ http playwright smtp traceroute ]
6
+
7
+ included do
8
+ attr_writer :logger
9
+
10
+ def logger
11
+ @logger || Rails.logger
12
+ end
13
+ end
14
+
15
+ class_methods do
16
+ def check_and_record_all_later
17
+ all.each(&:check_and_record_later)
18
+ end
19
+ end
20
+
21
+ def perform_check
22
+ # Overridden in subclasses that need setup around the actual check
23
+ check
24
+ end
25
+
26
+ def check_and_record_later
27
+ Upright::ProbeCheckJob.set(wait: self.class.stagger_delay).perform_later(self.class.name, name)
28
+ end
29
+
30
+ def check_and_record
31
+ result = failsafe_check
32
+
33
+ Upright::ProbeResult.create!(
34
+ probe_type: probe_type,
35
+ probe_name: probe_name,
36
+ probe_target: probe_target,
37
+ probe_service: probe_service,
38
+ status: result[:status],
39
+ duration: result[:duration],
40
+ error: result[:error]
41
+ ).tap do |probe_result|
42
+ on_check_recorded(probe_result)
43
+ end
44
+ end
45
+
46
+ def probe_name
47
+ try(:name) || self.class.name.demodulize.underscore.delete_suffix("_probe")
48
+ end
49
+
50
+ def probe_type
51
+ raise NotImplementedError
52
+ end
53
+
54
+ def probe_target
55
+ raise NotImplementedError
56
+ end
57
+
58
+ def on_check_recorded(probe_result)
59
+ raise NotImplementedError
60
+ end
61
+
62
+ def probe_service
63
+ nil
64
+ end
65
+
66
+ private
67
+ def failsafe_check
68
+ result, error, duration = nil
69
+
70
+ result, duration = measure { perform_check }
71
+ rescue => error
72
+ Rails.error.report(error)
73
+ raise error if Rails.env.development?
74
+ ensure
75
+ log_probe_result(result:, error:, duration:)
76
+
77
+ return { status: result_description(result, error), duration:, error: }
78
+ end
79
+
80
+ def result_description(result, error = nil)
81
+ if error || !result
82
+ :fail
83
+ else
84
+ :ok
85
+ end
86
+ end
87
+
88
+ def measure
89
+ start_time = monotonic_now
90
+ result = yield
91
+ [ result, monotonic_now - start_time ]
92
+ end
93
+
94
+ def monotonic_now
95
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
96
+ end
97
+
98
+ def log_probe_result(result:, error:, duration:)
99
+ current_site = Upright.current_site
100
+
101
+ log_data = {
102
+ probe: {
103
+ name: probe_name,
104
+ target: probe_target,
105
+ service: probe_service,
106
+ type: probe_type,
107
+ result: result_description(result, error),
108
+ duration: duration,
109
+ site_code: current_site.code,
110
+ site_city: current_site.city,
111
+ site_country: current_site.country,
112
+ site_geohash: current_site.geohash,
113
+ site_provider: current_site.provider,
114
+ error_class: (error.class.name if error),
115
+ error_message: (error.message if error)
116
+ }.compact
117
+ }
118
+
119
+ if logger.respond_to?(:struct)
120
+ logger.struct(**log_data)
121
+ else
122
+ logger.info(log_data.to_json)
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,22 @@
1
+ module Upright::Staggerable
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def stagger_by_site(interval)
6
+ self.stagger_interval = interval
7
+ end
8
+
9
+ def stagger_delay
10
+ if stagger_interval
11
+ current_site = Upright.current_site
12
+ stagger_interval * current_site.stagger_index
13
+ else
14
+ 0.seconds
15
+ end
16
+ end
17
+ end
18
+
19
+ included do
20
+ class_attribute :stagger_interval, default: nil
21
+ end
22
+ end
@@ -0,0 +1,108 @@
1
+ module Upright::Traceroute::OtelTracing
2
+ extend ActiveSupport::Concern
3
+
4
+ private
5
+ def tracer
6
+ @tracer ||= OpenTelemetry.tracer_provider.tracer("upright.traceroute", "1.0.0")
7
+ end
8
+
9
+ def trace_result(result)
10
+ tracer.in_span(root_span_name, attributes: root_span_attributes) do |root_span|
11
+ logger.struct probe_trace_id: root_span.context.hex_trace_id if logger.respond_to?(:struct)
12
+
13
+ result.hops.each_with_index do |hop, index|
14
+ previous_hop = index > 0 ? result.hops[index - 1] : nil
15
+ create_hop_span(hop, previous_hop)
16
+ end
17
+
18
+ root_span.status = if result.reached_destination?
19
+ success_status
20
+ else
21
+ error_status("Failed to reach destination")
22
+ end
23
+ end
24
+ end
25
+
26
+ def create_hop_span(hop, previous_hop)
27
+ previous_rtt_ms = previous_hop&.avg_ms.to_i
28
+ current_rtt_ms = hop.avg_ms.to_i
29
+ duration_ms = [ current_rtt_ms - previous_rtt_ms, 0 ].max
30
+
31
+ tracer.in_span(hop.display_name, attributes: hop_span_attributes(hop, previous_hop)) do |span|
32
+ span.status = hop_status(hop)
33
+ sleep(duration_ms / 1000.0)
34
+ end
35
+ end
36
+
37
+ def root_span_name
38
+ "traceroute #{host}"
39
+ end
40
+
41
+ def hop_status(hop)
42
+ if !hop.responded?
43
+ error_status("No response")
44
+ elsif hop.high_packet_loss?
45
+ error_status("High packet loss: #{hop.loss_percent}%")
46
+ elsif hop.any_packet_loss?
47
+ success_status("Packet loss: #{hop.loss_percent}%")
48
+ else
49
+ success_status
50
+ end
51
+ end
52
+
53
+ def success_status(message = nil)
54
+ if message
55
+ OpenTelemetry::Trace::Status.ok(message)
56
+ else
57
+ OpenTelemetry::Trace::Status.ok
58
+ end
59
+ end
60
+
61
+ def error_status(message)
62
+ OpenTelemetry::Trace::Status.error(message)
63
+ end
64
+
65
+ def root_span_attributes
66
+ site = Upright.current_site
67
+ {
68
+ "traceroute.target" => host,
69
+ "traceroute.max_hops" => self.class::MAX_HOPS,
70
+ "traceroute.probe_count" => self.class::PROBE_COUNT,
71
+ "upright.probe.name" => probe_name,
72
+ "upright.probe.type" => probe_type,
73
+ "upright.site.code" => site.code.to_s,
74
+ "upright.site.city" => site.city.to_s,
75
+ "upright.site.country" => site.country.to_s,
76
+ "upright.site.geohash" => site.geohash.to_s,
77
+ "upright.site.provider" => site.provider.to_s
78
+ }.compact_blank
79
+ end
80
+
81
+ def hop_span_attributes(hop, previous_hop)
82
+ {
83
+ "traceroute.hop.number" => hop.number,
84
+ "traceroute.hop.ip" => hop.ip,
85
+ "traceroute.hop.hostname" => hop.hostname,
86
+ "traceroute.hop.as" => hop.as,
87
+ "traceroute.hop.isp" => hop.isp,
88
+ "traceroute.hop.city" => hop.city,
89
+ "traceroute.hop.country" => hop.country,
90
+ "traceroute.hop.country_code" => hop.country_code,
91
+ "traceroute.hop.geohash" => hop.geohash,
92
+ "traceroute.hop.loss_percent" => hop.loss_percent,
93
+ "traceroute.hop.rtt_last_ms" => hop.last_ms,
94
+ "traceroute.hop.rtt_avg_ms" => hop.avg_ms,
95
+ "traceroute.hop.rtt_best_ms" => hop.best_ms,
96
+ "traceroute.hop.rtt_worst_ms" => hop.worst_ms,
97
+ "traceroute.hop.rtt_stddev_ms" => hop.stddev_ms,
98
+ "traceroute.hop.previous_ip" => previous_hop&.ip,
99
+ "traceroute.hop.previous_isp" => previous_hop&.isp,
100
+ "traceroute.hop.rtt_delta_ms" => rtt_delta_ms(hop, previous_hop)
101
+ }.compact
102
+ end
103
+
104
+ def rtt_delta_ms(hop, previous_hop)
105
+ delta = hop.avg_ms.to_i - previous_hop&.avg_ms.to_i
106
+ [ delta, 0 ].max
107
+ end
108
+ end
@@ -0,0 +1,3 @@
1
+ class Upright::ApplicationRecord < ActiveRecord::Base
2
+ self.abstract_class = true
3
+ end
@@ -0,0 +1,61 @@
1
+ class Upright::Artifact
2
+ ICONS = {
3
+ "webm" => "video",
4
+ "log" => "log",
5
+ "json" => "download",
6
+ "html" => "download",
7
+ "xml" => "download",
8
+ "txt" => "download",
9
+ "bin" => "download"
10
+ }.freeze
11
+
12
+ attr_reader :filename, :content
13
+
14
+ def initialize(name:, content:)
15
+ @filename = name
16
+ @content = content
17
+ end
18
+
19
+ def extension
20
+ File.extname(filename).delete_prefix(".")
21
+ end
22
+
23
+ def basename
24
+ File.basename(filename, ".*")
25
+ end
26
+
27
+ def timestamped_filename
28
+ current_site = Upright.current_site
29
+ [ Time.current.to_fs(:number), current_site.code, safe_name ].join("_") + ".#{extension}"
30
+ end
31
+
32
+ def content_type
33
+ Marcel::MimeType.for(extension: extension)
34
+ end
35
+
36
+ def icon
37
+ ICONS.fetch(extension, "attachment")
38
+ end
39
+
40
+ def attach_to(probe_result, timestamped: false)
41
+ probe_result.artifacts.attach(
42
+ io: to_io,
43
+ filename: timestamped ? timestamped_filename : filename,
44
+ content_type: content_type
45
+ )
46
+ end
47
+
48
+ private
49
+ def to_io
50
+ case content
51
+ when StringIO, File, IO
52
+ content.tap(&:rewind)
53
+ else
54
+ StringIO.new(content.to_s)
55
+ end
56
+ end
57
+
58
+ def safe_name
59
+ basename.parameterize(separator: "_")
60
+ end
61
+ end
@@ -0,0 +1,9 @@
1
+ class Upright::Current < ActiveSupport::CurrentAttributes
2
+ attribute :user
3
+ attribute :subdomain
4
+ attribute :site
5
+
6
+ def site
7
+ super || Upright.current_site
8
+ end
9
+ end
@@ -0,0 +1,59 @@
1
+ class Upright::HTTP::Request
2
+ attr_reader :url, :options
3
+
4
+ def initialize(url, **options)
5
+ @url = url
6
+ @options = options
7
+ end
8
+
9
+ def get
10
+ response = Typhoeus.get(url, request_options)
11
+ Upright::HTTP::Response.new(response, build_log(response))
12
+ end
13
+
14
+ private
15
+ def request_options
16
+ current_site = Upright.current_site
17
+
18
+ {
19
+ timeout: current_site.default_timeout,
20
+ connecttimeout: current_site.default_timeout,
21
+ headers: request_headers,
22
+ userpwd: userpwd,
23
+ proxy: proxy_url,
24
+ proxyuserpwd: proxy_userpwd,
25
+ verbose: true,
26
+ forbid_reuse: true
27
+ }.compact
28
+ end
29
+
30
+ def request_headers
31
+ { "User-Agent" => Upright.configuration.user_agent }
32
+ end
33
+
34
+ def userpwd
35
+ if options[:username] && options[:password]
36
+ "#{options[:username]}:#{options[:password]}"
37
+ end
38
+ end
39
+
40
+ def proxy_url
41
+ options[:proxy]
42
+ end
43
+
44
+ def proxy_userpwd
45
+ if options[:proxy_username] && options[:proxy_password]
46
+ "#{options[:proxy_username]}:#{options[:proxy_password]}"
47
+ end
48
+ end
49
+
50
+ def build_log(response)
51
+ StringIO.new.tap do |log|
52
+ if response.debug_info
53
+ response.debug_info.text.each { |msg| log.puts "* #{msg.chomp}" }
54
+ response.debug_info.header_out.each { |msg| log.puts "> #{msg.chomp}" }
55
+ response.debug_info.header_in.each { |msg| log.puts "< #{msg.chomp}" }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,55 @@
1
+ class Upright::HTTP::Response
2
+ def initialize(typhoeus_response, log)
3
+ @typhoeus_response = typhoeus_response
4
+ @log = log
5
+ end
6
+
7
+ def status
8
+ typhoeus_response.code
9
+ end
10
+
11
+ def body
12
+ typhoeus_response.body
13
+ end
14
+
15
+ def status_in?(range)
16
+ status.in?(range)
17
+ end
18
+
19
+ def network_error?
20
+ timed_out? || connection_failed? || ssl_error?
21
+ end
22
+
23
+ def content_type
24
+ typhoeus_response.headers&.[]("content-type") || "application/octet-stream"
25
+ end
26
+
27
+ def file_extension
28
+ case content_type
29
+ when /json/ then "json"
30
+ when /html/ then "html"
31
+ when /xml/ then "xml"
32
+ when /text/ then "txt"
33
+ else "bin"
34
+ end
35
+ end
36
+
37
+ def verbose_log_content
38
+ @log.tap(&:rewind).read
39
+ end
40
+
41
+ private
42
+ attr_reader :typhoeus_response
43
+
44
+ def timed_out?
45
+ typhoeus_response.timed_out?
46
+ end
47
+
48
+ def connection_failed?
49
+ typhoeus_response.return_code == :couldnt_connect
50
+ end
51
+
52
+ def ssl_error?
53
+ typhoeus_response.return_code.to_s.start_with?("ssl")
54
+ end
55
+ end