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,57 @@
1
+ require "omniauth"
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class StaticCredentials
6
+ include OmniAuth::Strategy
7
+
8
+ option :name, "static_credentials"
9
+ option :title, "Sign In"
10
+ option :credentials, {}
11
+
12
+ def request_phase
13
+ OmniAuth::Form.build(title: options.title, url: callback_path) do
14
+ text_field "Username", "username"
15
+ password_field "Password", "password"
16
+ end.to_response
17
+ end
18
+
19
+ def callback_phase
20
+ if valid_credentials?
21
+ super
22
+ else
23
+ fail!(:invalid_credentials)
24
+ end
25
+ end
26
+
27
+ uid { username }
28
+
29
+ info do
30
+ { name: username, email: "#{username}@localhost" }
31
+ end
32
+
33
+ protected
34
+
35
+ def valid_credentials?
36
+ return false if username.blank? || password.blank?
37
+
38
+ configured_credentials.any? do |user, pass|
39
+ ActiveSupport::SecurityUtils.secure_compare(username, user.to_s) &&
40
+ ActiveSupport::SecurityUtils.secure_compare(password, pass.to_s)
41
+ end
42
+ end
43
+
44
+ def configured_credentials
45
+ options.credentials || {}
46
+ end
47
+
48
+ def username
49
+ request.params["username"]
50
+ end
51
+
52
+ def password
53
+ request.params["password"]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :upright do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,106 @@
1
+ class Upright::Configuration
2
+ # Global subdomain is always "app" - this is documented behavior
3
+ GLOBAL_SUBDOMAIN = "app"
4
+
5
+ # Core settings
6
+ attr_accessor :service_name
7
+ attr_accessor :user_agent
8
+ attr_accessor :default_timeout
9
+
10
+ # Storage paths
11
+ attr_accessor :prometheus_dir
12
+ attr_accessor :video_storage_dir
13
+ attr_accessor :storage_state_dir
14
+ attr_accessor :frozen_record_path
15
+
16
+ # Probe and authenticator paths (for auto-loading app-specific code)
17
+ attr_writer :probes_path
18
+ attr_writer :authenticators_path
19
+
20
+ # Playwright
21
+ attr_accessor :playwright_server_url
22
+
23
+ # Authentication
24
+ attr_accessor :auth_provider
25
+ attr_accessor :auth_options
26
+
27
+ # Observability
28
+ attr_accessor :otel_endpoint
29
+ attr_accessor :prometheus_url
30
+ attr_accessor :alert_webhook_url
31
+
32
+ def initialize
33
+ @service_name = "upright"
34
+ @user_agent = "Upright/1.0"
35
+ @default_timeout = 10.seconds
36
+
37
+ @prometheus_dir = nil
38
+ @video_storage_dir = nil
39
+ @storage_state_dir = nil
40
+ @frozen_record_path = nil
41
+ @probes_path = nil
42
+ @authenticators_path = nil
43
+
44
+ @playwright_server_url = ENV["PLAYWRIGHT_SERVER_URL"]
45
+ @otel_endpoint = ENV["OTEL_EXPORTER_OTLP_ENDPOINT"]
46
+
47
+ @auth_provider = :static_credentials
48
+ @auth_options = {}
49
+ end
50
+
51
+ def global_subdomain
52
+ GLOBAL_SUBDOMAIN
53
+ end
54
+
55
+ def site_subdomains
56
+ Upright.sites.map { |site| site.code.to_s }
57
+ end
58
+
59
+ def prometheus_dir
60
+ @prometheus_dir || Rails.root.join("tmp", "prometheus")
61
+ end
62
+
63
+ def video_storage_dir
64
+ @video_storage_dir || Rails.root.join("storage", "playwright_videos")
65
+ end
66
+
67
+ def storage_state_dir
68
+ @storage_state_dir || Rails.root.join("storage", "playwright_storage_states")
69
+ end
70
+
71
+ def frozen_record_path
72
+ @frozen_record_path || Rails.root.join("config", "probes")
73
+ end
74
+
75
+ def probes_path
76
+ @probes_path || Rails.root.join("probes")
77
+ end
78
+
79
+ def authenticators_path
80
+ @authenticators_path || Rails.root.join("probes", "authenticators")
81
+ end
82
+
83
+ def hostname=(value)
84
+ @hostname = value
85
+ configure_allowed_hosts
86
+ end
87
+
88
+ def hostname
89
+ @hostname
90
+ end
91
+
92
+ def default_url_options
93
+ if Rails.env.production?
94
+ { protocol: "https", host: "#{global_subdomain}.#{hostname}", domain: hostname }
95
+ else
96
+ { protocol: "http", host: "#{global_subdomain}.#{hostname}", port: ENV.fetch("PORT", 3000).to_i, domain: hostname }
97
+ end
98
+ end
99
+
100
+ private
101
+ def configure_allowed_hosts
102
+ port_suffix = Rails.env.local? ? "(:\\d+)?" : ""
103
+ Rails.application.config.hosts = [ /.*\.#{Regexp.escape(hostname)}#{port_suffix}/, /#{Regexp.escape(hostname)}#{port_suffix}/ ]
104
+ Rails.application.config.action_dispatch.tld_length = 1
105
+ end
106
+ end
@@ -0,0 +1,157 @@
1
+ class Upright::Engine < ::Rails::Engine
2
+ isolate_namespace Upright
3
+
4
+ # Add concerns to autoload paths
5
+ config.autoload_paths << root.join("app/models/concerns")
6
+
7
+ # Session store configuration
8
+ initializer "upright.session_store", before: :load_config_initializers do |app|
9
+ app.config.session_store :cookie_store,
10
+ key: "_upright_session",
11
+ domain: :all,
12
+ same_site: :lax,
13
+ secure: Rails.env.production?,
14
+ expire_after: 24.hours
15
+ end
16
+
17
+ config.after_initialize do
18
+ url_options = Upright.configuration.default_url_options
19
+ Rails.application.routes.default_url_options = url_options
20
+ Upright::Engine.routes.default_url_options = url_options
21
+ end
22
+
23
+ initializer "upright.solid_queue", before: :set_configs_for_current_railties do |app|
24
+ unless Rails.env.test?
25
+ app.config.active_job.queue_adapter = :solid_queue
26
+ app.config.solid_queue.connects_to = { database: { writing: :queue, reading: :queue } }
27
+ end
28
+ end
29
+
30
+ # Configure Mission Control to use engine's authenticated controller
31
+ initializer "upright.mission_control" do
32
+ MissionControl::Jobs.base_controller_class = "Upright::ApplicationController"
33
+ MissionControl::Jobs.http_basic_auth_enabled = false
34
+ end
35
+
36
+ # Configure acronym inflections for autoloading
37
+ initializer "upright.inflections", before: :bootstrap_hook do
38
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
39
+ inflect.acronym "HTTP"
40
+ inflect.acronym "SMTP"
41
+ end
42
+ end
43
+
44
+ config.generators do |g|
45
+ g.test_framework :minitest
46
+ end
47
+
48
+ initializer "upright.assets" do |app|
49
+ app.config.assets.paths << root.join("app/javascript")
50
+ end
51
+
52
+ initializer "upright.importmap", before: "importmap" do |app|
53
+ if defined?(Importmap::Engine)
54
+ app.config.importmap.paths << root.join("config/importmap.rb")
55
+ app.config.importmap.cache_sweepers << root.join("app/javascript")
56
+ end
57
+ end
58
+
59
+ initializer "upright.frozen_record" do
60
+ FrozenRecord::Base.base_path = Upright.configuration.frozen_record_path
61
+ end
62
+
63
+ initializer "upright.yabeda" do
64
+ Upright::Metrics.configure
65
+ end
66
+
67
+ initializer "upright.opentelemetry" do
68
+ Upright::Tracing.configure
69
+ end
70
+
71
+ # Start metrics server for Solid Queue worker process
72
+ initializer "upright.solid_queue_metrics" do
73
+ SolidQueue.on_start do
74
+ ENV["PROMETHEUS_EXPORTER_PORT"] ||= Rails.env.local? ? "9395" : "9394"
75
+ ENV["PROMETHEUS_EXPORTER_LOG_REQUESTS"] = "false"
76
+ Yabeda::Prometheus::Exporter.start_metrics_server!
77
+ end
78
+ end
79
+
80
+ initializer "upright.duration_extension" do
81
+ ActiveSupport::Duration.class_eval do
82
+ def in_ms
83
+ (to_f * 1000).to_i
84
+ end
85
+ end
86
+ end
87
+
88
+ # Silence Ethon's verbose debug output to stdout
89
+ # By default, Ethon's debug callback prints curl verbose messages which pollutes logs
90
+ initializer "upright.ethon" do
91
+ Ethon::Easy::Callbacks.module_eval do
92
+ def debug_callback
93
+ @debug_callback ||= proc { |handle, type, data, size, udata|
94
+ message = data.read_string(size)
95
+ @debug_info.add(type, message)
96
+ 0
97
+ }
98
+ end
99
+ end
100
+ end
101
+
102
+ # Allow host app to override views
103
+ config.to_prepare do
104
+ Upright::ApplicationController.helper Rails.application.helpers
105
+ end
106
+
107
+ # Print available URLs in development
108
+ config.after_initialize do
109
+ if Rails.env.development? && defined?(Rails::Server)
110
+ url_options = Upright.configuration.default_url_options
111
+ hostname = url_options[:domain]
112
+ port = url_options[:port]
113
+ protocol = url_options[:protocol]
114
+
115
+ puts ""
116
+ puts "Upright is running at:"
117
+ puts " Global: #{protocol}://#{Upright.configuration.global_subdomain}.#{hostname}:#{port}"
118
+ Upright.sites.each do |site|
119
+ puts " #{site.city || site.code}: #{protocol}://#{site.code}.#{hostname}:#{port}"
120
+ end
121
+ puts ""
122
+ end
123
+ end
124
+
125
+ # Auto-load Playwright probes and authenticators from configured paths
126
+ config.after_initialize do
127
+ # Define namespaces for app-specific probes and authenticators
128
+ module ::Probes
129
+ module Playwright
130
+ end
131
+ end
132
+
133
+ module ::Playwright
134
+ module Authenticator
135
+ end
136
+ end
137
+
138
+ probes_path = Upright.configuration.probes_path
139
+ if probes_path && Dir.exist?(probes_path)
140
+ Dir[probes_path.join("*_probe.rb")].sort.each { |file| require file }
141
+ end
142
+
143
+ authenticators_path = Upright.configuration.authenticators_path
144
+ if authenticators_path && Dir.exist?(authenticators_path)
145
+ Dir[authenticators_path.join("*.rb")].sort.each { |file| require file }
146
+ end
147
+ end
148
+
149
+ # Add engine migrations to host app
150
+ initializer "upright.migrations" do |app|
151
+ unless app.root.to_s == root.join("test/dummy").to_s
152
+ config.paths["db/migrate"].expanded.each do |expanded_path|
153
+ app.config.paths["db/migrate"] << expanded_path
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,62 @@
1
+ require "prometheus/client"
2
+ require "prometheus/client/data_stores/direct_file_store"
3
+ require "yabeda"
4
+
5
+ module Upright::Metrics
6
+ class << self
7
+ def configure
8
+ setup_prometheus_store unless Rails.env.test?
9
+ define_metrics
10
+ Yabeda.configure!
11
+ end
12
+
13
+ def probe_duration_seconds
14
+ Yabeda.upright_probe_duration_seconds
15
+ end
16
+
17
+ def probe_up
18
+ Yabeda.upright_probe_up
19
+ end
20
+
21
+ def http_response_status
22
+ Yabeda.upright_http_response_status
23
+ end
24
+
25
+ private
26
+ def setup_prometheus_store
27
+ prometheus_dir = Upright.configuration.prometheus_dir
28
+ FileUtils.mkdir_p(prometheus_dir)
29
+
30
+ Prometheus::Client.config.data_store = Prometheus::Client::DataStores::DirectFileStore.new(dir: prometheus_dir.to_s)
31
+ end
32
+
33
+ def define_metrics
34
+ current_site = Upright.current_site
35
+
36
+ Yabeda.configure do
37
+ default_tag :site_code, current_site&.code
38
+ default_tag :site_city, current_site&.city
39
+ default_tag :site_country, current_site&.country
40
+ default_tag :site_geohash, current_site&.geohash
41
+ default_tag :site_provider, current_site&.provider
42
+
43
+ group :upright do
44
+ gauge :probe_duration_seconds,
45
+ comment: "Duration of each probe",
46
+ aggregation: :max,
47
+ tags: %i[type name probe_target probe_service status]
48
+
49
+ gauge :probe_up,
50
+ comment: "Probe status (1 = up, 0 = down)",
51
+ aggregation: :most_recent,
52
+ tags: %i[type name probe_target probe_service]
53
+
54
+ gauge :http_response_status,
55
+ comment: "HTTP response status code",
56
+ aggregation: :max,
57
+ tags: %i[name probe_target probe_service]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,36 @@
1
+ (() => {
2
+ const metrics = {};
3
+
4
+ const [navigation] = performance.getEntriesByType("navigation");
5
+ if (navigation) {
6
+ metrics.ttfb = Math.round(navigation.responseStart);
7
+ metrics.dns = Math.round(navigation.domainLookupEnd - navigation.domainLookupStart);
8
+ metrics.tcp = Math.round(navigation.connectEnd - navigation.connectStart);
9
+ metrics.request = Math.round(navigation.responseStart - navigation.requestStart);
10
+ metrics.response = Math.round(navigation.responseEnd - navigation.responseStart);
11
+ metrics.domInteractive = Math.round(navigation.domInteractive);
12
+ metrics.domContentLoaded = Math.round(navigation.domContentLoadedEventEnd);
13
+ metrics.domComplete = Math.round(navigation.domComplete);
14
+ metrics.loadComplete = Math.round(navigation.loadEventEnd);
15
+ metrics.transferSize = navigation.transferSize;
16
+ metrics.encodedBodySize = navigation.encodedBodySize;
17
+ metrics.decodedBodySize = navigation.decodedBodySize;
18
+ }
19
+
20
+ const [fcp] = performance.getEntriesByType("paint").filter(e => e.name === "first-contentful-paint");
21
+ if (fcp) {
22
+ metrics.fcp = Math.round(fcp.startTime);
23
+ }
24
+
25
+ const lcpEntries = performance.getEntriesByType("largest-contentful-paint");
26
+ if (lcpEntries.length > 0) {
27
+ metrics.lcp = Math.round(lcpEntries[lcpEntries.length - 1].startTime);
28
+ }
29
+
30
+ const [fid] = performance.getEntriesByType("first-input");
31
+ if (fid) {
32
+ metrics.fid = Math.round(fid.processingStart - fid.startTime);
33
+ }
34
+
35
+ return metrics;
36
+ })();
@@ -0,0 +1,49 @@
1
+ require "geohash_ruby"
2
+
3
+ module Upright
4
+ class Site
5
+ attr_reader :code, :city, :country, :geohash, :stagger_index
6
+
7
+ def initialize(code:, city: nil, country: nil, geohash: nil, provider: nil, stagger_index: 0)
8
+ @code = code.to_sym
9
+ @city = city
10
+ @country = country
11
+ @geohash = geohash
12
+ @provider = provider
13
+ @stagger_index = stagger_index
14
+ end
15
+
16
+ def host
17
+ URI.parse(url).host
18
+ end
19
+
20
+ def provider
21
+ @provider.to_s.inquiry
22
+ end
23
+
24
+ def default_timeout
25
+ Upright.configuration.default_timeout
26
+ end
27
+
28
+ def latitude
29
+ coordinates.first
30
+ end
31
+
32
+ def longitude
33
+ coordinates.last
34
+ end
35
+
36
+ def url
37
+ Upright::Engine.routes.url_helpers.root_url(subdomain: code)
38
+ end
39
+
40
+ def to_leaflet
41
+ { hostname: host, city: city, lat: latitude, lon: longitude, url: url }
42
+ end
43
+
44
+ private
45
+ def coordinates
46
+ @coordinates ||= Geohash.decode(geohash).first
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ module Upright::Tracing
2
+ class << self
3
+ def configure
4
+ current_site = Upright.current_site
5
+
6
+ OpenTelemetry::SDK.configure do |c|
7
+ c.service_name = Upright.configuration.service_name
8
+ c.service_version = Upright::VERSION
9
+
10
+ c.resource = OpenTelemetry::SDK::Resources::Resource.create(
11
+ resource_attributes(current_site)
12
+ )
13
+
14
+ # Use OTLP exporter if endpoint is configured
15
+ if Upright.configuration.otel_endpoint
16
+ c.add_span_processor(
17
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
18
+ OpenTelemetry::Exporter::OTLP::Exporter.new(
19
+ endpoint: Upright.configuration.otel_endpoint
20
+ )
21
+ )
22
+ )
23
+ end
24
+
25
+ c.use_all
26
+ end
27
+ end
28
+
29
+ def tracer
30
+ OpenTelemetry.tracer_provider.tracer(Upright.configuration.service_name, Upright::VERSION)
31
+ end
32
+
33
+ def with_span(name, attributes: {}, &block)
34
+ tracer.in_span(name, attributes: attributes, &block)
35
+ end
36
+
37
+ private
38
+ def resource_attributes(site)
39
+ {
40
+ "deployment.environment" => Rails.env.to_s,
41
+ "site.code" => site.code.to_s,
42
+ "site.city" => site.city.to_s,
43
+ "site.country" => site.country.to_s,
44
+ "site.geohash" => site.geohash.to_s,
45
+ "site.provider" => site.provider.to_s
46
+ }.compact_blank
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ module Upright
2
+ VERSION = "0.1.0"
3
+ end
data/lib/upright.rb ADDED
@@ -0,0 +1,68 @@
1
+ require "frozen_record"
2
+ require "prometheus/api_client"
3
+ require "opentelemetry-sdk"
4
+ require "opentelemetry-exporter-otlp"
5
+ require "typhoeus"
6
+ require "solid_queue"
7
+ require "mission_control/jobs"
8
+ require "omniauth"
9
+ require "omniauth_openid_connect"
10
+ require "omniauth/rails_csrf_protection"
11
+ require "omniauth/strategies/static_credentials"
12
+ require "propshaft"
13
+ require "importmap-rails"
14
+ require "turbo-rails"
15
+ require "stimulus-rails"
16
+ require "geared_pagination"
17
+ require "geohash_ruby"
18
+ require "yabeda/prometheus"
19
+ require "yabeda/puma/plugin"
20
+
21
+ require "upright/version"
22
+ require "upright/configuration"
23
+ require "upright/site"
24
+ require "upright/metrics"
25
+ require "upright/tracing"
26
+ require "upright/engine"
27
+
28
+ module Upright
29
+ class ConfigurationError < StandardError; end
30
+
31
+ class << self
32
+ def configuration
33
+ @configuration ||= Configuration.new
34
+ end
35
+ alias_method :config, :configuration
36
+
37
+ def configure
38
+ yield(configuration)
39
+ end
40
+
41
+ def sites
42
+ @sites ||= load_sites
43
+ end
44
+
45
+ def find_site(code)
46
+ sites.find { |site| site.code.to_s == code.to_s }
47
+ end
48
+
49
+ def current_site
50
+ find_site(ENV["SITE_SUBDOMAIN"]) || sites.first
51
+ end
52
+
53
+ private
54
+ def load_sites
55
+ sites_config_path = Rails.root.join("config/sites.yml")
56
+
57
+ if sites_config_path.exist?
58
+ config = Rails.application.config_for(:sites)
59
+
60
+ config[:sites].map.with_index do |site_config, index|
61
+ Site.new(stagger_index: index, **site_config)
62
+ end
63
+ else
64
+ []
65
+ end
66
+ end
67
+ end
68
+ end