flare 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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +148 -0
  5. data/app/controllers/flare/application_controller.rb +22 -0
  6. data/app/controllers/flare/jobs_controller.rb +55 -0
  7. data/app/controllers/flare/requests_controller.rb +73 -0
  8. data/app/controllers/flare/spans_controller.rb +101 -0
  9. data/app/helpers/flare/application_helper.rb +168 -0
  10. data/app/views/flare/jobs/index.html.erb +69 -0
  11. data/app/views/flare/jobs/show.html.erb +323 -0
  12. data/app/views/flare/requests/index.html.erb +120 -0
  13. data/app/views/flare/requests/show.html.erb +498 -0
  14. data/app/views/flare/spans/index.html.erb +112 -0
  15. data/app/views/flare/spans/show.html.erb +184 -0
  16. data/app/views/layouts/flare/application.html.erb +126 -0
  17. data/config/routes.rb +20 -0
  18. data/exe/flare +9 -0
  19. data/lib/flare/backoff_policy.rb +73 -0
  20. data/lib/flare/cli/doctor_command.rb +129 -0
  21. data/lib/flare/cli/output.rb +45 -0
  22. data/lib/flare/cli/setup_command.rb +404 -0
  23. data/lib/flare/cli/status_command.rb +47 -0
  24. data/lib/flare/cli.rb +50 -0
  25. data/lib/flare/configuration.rb +121 -0
  26. data/lib/flare/engine.rb +43 -0
  27. data/lib/flare/http_metrics_config.rb +101 -0
  28. data/lib/flare/metric_counter.rb +45 -0
  29. data/lib/flare/metric_flusher.rb +124 -0
  30. data/lib/flare/metric_key.rb +42 -0
  31. data/lib/flare/metric_span_processor.rb +470 -0
  32. data/lib/flare/metric_storage.rb +42 -0
  33. data/lib/flare/metric_submitter.rb +221 -0
  34. data/lib/flare/source_location.rb +113 -0
  35. data/lib/flare/sqlite_exporter.rb +279 -0
  36. data/lib/flare/storage/sqlite.rb +789 -0
  37. data/lib/flare/storage.rb +54 -0
  38. data/lib/flare/version.rb +5 -0
  39. data/lib/flare.rb +411 -0
  40. data/public/flare-assets/flare.css +1245 -0
  41. data/public/flare-assets/images/flipper.png +0 -0
  42. metadata +240 -0
@@ -0,0 +1,404 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "digest"
5
+ require "base64"
6
+ require "socket"
7
+ require "net/http"
8
+ require "uri"
9
+ require "json"
10
+ require "fileutils"
11
+ require_relative "output"
12
+
13
+ module Flare
14
+ class SetupCommand
15
+ include CLI::Output
16
+
17
+ DEFAULT_HOST = "https://flare.am"
18
+ TIMEOUT_SECONDS = 300 # 5 minutes
19
+
20
+ INITIALIZER_CONTENT = <<~RUBY
21
+ # frozen_string_literal: true
22
+
23
+ Flare.configure do |config|
24
+ # ── Spans (local development dashboard) ────────────────────────────
25
+ # Spans capture detailed trace data and are stored in a local SQLite
26
+ # database. Enabled by default in development only. Visit /flare in
27
+ # your browser to see the dashboard.
28
+
29
+ # Enable or disable spans (default: true in development)
30
+ # config.spans_enabled = true
31
+
32
+ # How long to keep spans in hours (default: 24)
33
+ # config.retention_hours = 24
34
+
35
+ # Maximum number of spans to store (default: 10000)
36
+ # config.max_spans = 10000
37
+
38
+ # Path to the SQLite database (default: db/flare.sqlite3)
39
+ # config.database_path = Rails.root.join("db", "flare.sqlite3").to_s
40
+
41
+ # Ignore specific requests (receives a Rack::Request, return true to ignore)
42
+ # config.ignore_request = ->(request) {
43
+ # request.path.start_with?("/health")
44
+ # }
45
+
46
+ # ── Metrics (remote monitoring) ────────────────────────────────────
47
+ # Metrics aggregate span data into counts, durations, and error rates.
48
+ # Enabled by default in development and production (disabled in test).
49
+ # Sent to flare.am when FLARE_KEY is configured.
50
+
51
+ # Enable or disable metrics (default: true except in test)
52
+ # config.metrics_enabled = true
53
+
54
+ # How often to flush metrics in seconds (default: 60)
55
+ # config.metrics_flush_interval = 60
56
+
57
+ # ── Custom Instrumentation ─────────────────────────────────────────
58
+ # Subscribe to additional notification prefixes (default: ["app."])
59
+ # config.subscribe_patterns << "mycompany."
60
+ end
61
+
62
+ # ════════════════════════════════════════════════════════════════════════
63
+ # Custom Instrumentation
64
+ # ════════════════════════════════════════════════════════════════════════
65
+ #
66
+ # Use ActiveSupport::Notifications.instrument with an "app." prefix
67
+ # anywhere in your code. Flare captures these in development and
68
+ # displays them in the dashboard.
69
+ #
70
+ # ActiveSupport::Notifications.instrument("app.geocoding", address: address) do
71
+ # geocoder.lookup(address)
72
+ # end
73
+ #
74
+ # ActiveSupport::Notifications.instrument("app.stripe.charge", amount: 1000) do
75
+ # Stripe::Charge.create(amount: 1000, currency: "usd")
76
+ # end
77
+ RUBY
78
+
79
+ def initialize(force: false)
80
+ @force = force
81
+ end
82
+
83
+ def run
84
+ authenticate
85
+ create_initializer
86
+ add_gitignore_entries
87
+
88
+ puts
89
+ puts "#{green("Setup complete!")}"
90
+ puts
91
+ puts bold("What's next:")
92
+ puts " 1. Start your Rails server (#{dim("bin/rails server")})"
93
+ puts " 2. Make a few requests to your app"
94
+ puts " 3. Visit #{bold("/flare")} to see the dashboard"
95
+ puts
96
+ puts dim(" The dashboard auto-mounts at /flare in development.")
97
+ puts dim(" Metrics are sent to flare.am when FLARE_KEY is configured.")
98
+ puts
99
+ puts " Run #{bold("flare doctor")} to verify your setup."
100
+ puts " Run #{bold("flare status")} to see your configuration."
101
+ rescue Interrupt
102
+ puts
103
+ puts "Setup cancelled."
104
+ exit 1
105
+ end
106
+
107
+ private
108
+
109
+ # --- Auth ---
110
+
111
+ def authenticate
112
+ env_path = File.join(Dir.pwd, ".env")
113
+
114
+ if !@force && File.exist?(env_path) && File.read(env_path).match?(/^FLARE_KEY=.+/)
115
+ puts "#{checkmark} FLARE_KEY already set in .env, skipping auth."
116
+ puts " Run with --force to re-authenticate."
117
+ return
118
+ end
119
+
120
+ server = nil
121
+ state = SecureRandom.hex(32)
122
+ code_verifier = SecureRandom.urlsafe_base64(32)
123
+ code_challenge = Base64.urlsafe_encode64(
124
+ Digest::SHA256.digest(code_verifier),
125
+ padding: false
126
+ )
127
+
128
+ server = TCPServer.new("127.0.0.1", 0)
129
+ port = server.addr[1]
130
+
131
+ host = ENV.fetch("FLARE_HOST", DEFAULT_HOST)
132
+ authorize_url = "#{host}/cli/authorize?state=#{state}&port=#{port}&code_challenge=#{code_challenge}"
133
+
134
+ puts "Opening browser to authorize Flare..."
135
+ open_browser(authorize_url)
136
+ puts
137
+ puts "If the browser didn't open, visit:"
138
+ puts " #{authorize_url}"
139
+ puts
140
+ puts "Waiting for authorization (up to 5 minutes)..."
141
+
142
+ auth_code = wait_for_callback(server, state, TIMEOUT_SECONDS)
143
+
144
+ unless auth_code
145
+ puts "Timed out waiting for authorization."
146
+ exit 1
147
+ end
148
+
149
+ token = exchange_code(auth_code, code_verifier)
150
+ save_token(token)
151
+ ensure
152
+ server&.close
153
+ end
154
+
155
+ def wait_for_callback(server, expected_state, timeout)
156
+ deadline = Time.now + timeout
157
+
158
+ while Time.now < deadline
159
+ readable = IO.select([server], nil, nil, 1)
160
+ next unless readable
161
+
162
+ client = server.accept
163
+ request_line = client.gets
164
+
165
+ unless request_line&.start_with?("GET /callback")
166
+ client.print "HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n"
167
+ client.close
168
+ next
169
+ end
170
+
171
+ # Read remaining headers (required by HTTP spec)
172
+ while (line = client.gets) && line != "\r\n"; end
173
+
174
+ params = parse_query_string(request_line)
175
+ returned_state = params["state"]
176
+ code = params["code"]
177
+ error = params["error"]
178
+
179
+ if error
180
+ client.print http_response(error_page(error))
181
+ client.close
182
+ return nil
183
+ elsif returned_state != expected_state
184
+ client.print http_response(error_page("State mismatch. Please try again."))
185
+ client.close
186
+ return nil
187
+ elsif code.nil? || code.empty?
188
+ client.print http_response(error_page("No authorization code received."))
189
+ client.close
190
+ return nil
191
+ else
192
+ client.print http_response(success_page)
193
+ client.close
194
+ return code
195
+ end
196
+ end
197
+
198
+ nil
199
+ end
200
+
201
+ def parse_query_string(request_line)
202
+ return {} unless request_line
203
+ path = request_line.split(" ")[1]
204
+ return {} unless path
205
+ query = URI(path).query
206
+ return {} unless query
207
+ URI.decode_www_form(query).to_h
208
+ end
209
+
210
+ def http_response(body)
211
+ "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}"
212
+ end
213
+
214
+ def exchange_code(auth_code, code_verifier)
215
+ host = ENV.fetch("FLARE_HOST", DEFAULT_HOST)
216
+ uri = URI("#{host}/api/cli/exchange")
217
+ http = Net::HTTP.new(uri.host, uri.port)
218
+ http.use_ssl = uri.scheme == "https"
219
+
220
+ request = Net::HTTP::Post.new(uri.path)
221
+ request["Content-Type"] = "application/x-www-form-urlencoded"
222
+ request.set_form_data(code: auth_code, code_verifier: code_verifier)
223
+
224
+ response = http.request(request)
225
+
226
+ unless response.is_a?(Net::HTTPSuccess)
227
+ $stderr.puts "Failed to exchange code for token: #{response.code} #{response.message}"
228
+ $stderr.puts response.body if response.body
229
+ exit 1
230
+ end
231
+
232
+ data = JSON.parse(response.body)
233
+ token = data["token"]
234
+
235
+ if token.nil? || token.empty?
236
+ $stderr.puts "No token received from server."
237
+ exit 1
238
+ end
239
+
240
+ token
241
+ end
242
+
243
+ # --- Token storage ---
244
+
245
+ def save_token(token)
246
+ puts
247
+ puts "Where would you like to save the token?"
248
+ puts " 1. .env file"
249
+ puts " 2. Rails credentials"
250
+ puts " 3. Print token"
251
+ print "Choose (1/2/3): "
252
+
253
+ choice = $stdin.gets&.strip
254
+
255
+ case choice
256
+ when "1"
257
+ @saved_to_dotenv = true
258
+ save_to_dotenv(token)
259
+ when "2"
260
+ print_credentials_instructions(token)
261
+ when "3"
262
+ print_token(token)
263
+ else
264
+ puts "Invalid choice."
265
+ save_token(token)
266
+ end
267
+ end
268
+
269
+ def save_to_dotenv(token)
270
+ env_path = File.join(Dir.pwd, ".env")
271
+
272
+ if File.exist?(env_path)
273
+ contents = File.read(env_path)
274
+ if contents.match?(/^FLARE_KEY=/)
275
+ contents.gsub!(/^FLARE_KEY=.*$/, "FLARE_KEY=#{token}")
276
+ else
277
+ contents = contents.chomp + "\nFLARE_KEY=#{token}\n"
278
+ end
279
+ File.write(env_path, contents)
280
+ puts " Token saved to .env"
281
+ else
282
+ print " .env file not found. Create it? (y/n): "
283
+ answer = $stdin.gets&.strip&.downcase
284
+ if answer == "y" || answer == "yes"
285
+ File.write(env_path, "FLARE_KEY=#{token}\n")
286
+ puts " Created .env with FLARE_KEY"
287
+ else
288
+ print_dotenv_instructions(token)
289
+ end
290
+ end
291
+ end
292
+
293
+ def print_dotenv_instructions(token)
294
+ puts
295
+ puts "Add the following to your .env file:"
296
+ puts
297
+ puts " FLARE_KEY=#{token}"
298
+ puts
299
+ puts "Make sure .env is loaded in your app, for example with the dotenv gem:"
300
+ puts
301
+ puts " # Gemfile"
302
+ puts " gem \"dotenv-rails\", groups: [:development, :test]"
303
+ end
304
+
305
+ def print_credentials_instructions(token)
306
+ puts
307
+ puts "Add the following to your Rails credentials:"
308
+ puts
309
+ puts " bin/rails credentials:edit"
310
+ puts
311
+ puts " flare:"
312
+ puts " key: #{token}"
313
+ puts
314
+ puts "Or for a specific environment:"
315
+ puts
316
+ puts " bin/rails credentials:edit --environment production"
317
+ puts
318
+ puts " flare:"
319
+ puts " key: #{token}"
320
+ puts
321
+ end
322
+
323
+ def print_token(token)
324
+ puts
325
+ puts " FLARE_KEY=#{token}"
326
+ end
327
+
328
+ # --- Project setup ---
329
+
330
+ def create_initializer
331
+ path = File.join(Dir.pwd, "config/initializers/flare.rb")
332
+ existed = File.exist?(path)
333
+
334
+ if existed && !@force
335
+ puts "#{checkmark} config/initializers/flare.rb already exists"
336
+ puts " Run with --force to overwrite."
337
+ return
338
+ end
339
+
340
+ FileUtils.mkdir_p(File.dirname(path))
341
+ File.write(path, INITIALIZER_CONTENT)
342
+
343
+ if existed
344
+ puts "#{checkmark} Overwrote config/initializers/flare.rb"
345
+ else
346
+ puts "#{checkmark} Created config/initializers/flare.rb"
347
+ end
348
+ end
349
+
350
+ def add_gitignore_entries
351
+ gitignore_path = File.join(Dir.pwd, ".gitignore")
352
+ return unless File.exist?(gitignore_path)
353
+
354
+ contents = File.read(gitignore_path)
355
+ entries_to_add = []
356
+
357
+ entries_to_add << ".env" if @saved_to_dotenv && !contents.match?(/^\.env$/)
358
+ entries_to_add << "flare.sqlite3*" unless contents.include?("flare.sqlite3*")
359
+
360
+ return if entries_to_add.empty?
361
+
362
+ File.open(gitignore_path, "a") do |f|
363
+ f.puts "" unless contents.end_with?("\n")
364
+ entries_to_add.each { |entry| f.puts entry }
365
+ end
366
+
367
+ puts "#{checkmark} Added #{entries_to_add.join(", ")} to .gitignore"
368
+ end
369
+
370
+ # --- Browser ---
371
+
372
+ def open_browser(url)
373
+ case RUBY_PLATFORM
374
+ when /darwin/ then system("open", url)
375
+ when /linux/ then system("xdg-open", url)
376
+ when /mingw|mswin/ then system("start", url)
377
+ end
378
+ end
379
+
380
+ def success_page
381
+ page_shell("Authorized!", "You can close this window.")
382
+ end
383
+
384
+ def error_page(message)
385
+ escaped = message.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
386
+ page_shell("Error", escaped)
387
+ end
388
+
389
+ def page_shell(title, subtitle)
390
+ <<~HTML
391
+ <!DOCTYPE html>
392
+ <html>
393
+ <head><title>Flare</title></head>
394
+ <body style="font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f5f3ef;">
395
+ <div style="text-align: center; background: #fff; border-radius: 16px; padding: 48px 56px; box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.04);">
396
+ <h1 style="color: #3d3529; font-size: 28px; font-weight: 700; margin: 0 0 8px;">#{title}</h1>
397
+ <p style="color: #8a8078; font-size: 15px; margin: 0;">#{subtitle}</p>
398
+ </div>
399
+ </body>
400
+ </html>
401
+ HTML
402
+ end
403
+ end
404
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "output"
4
+ require_relative "../version"
5
+
6
+ module Flare
7
+ class StatusCommand
8
+ include CLI::Output
9
+
10
+ def run
11
+ puts bold("Flare v#{VERSION}")
12
+ puts
13
+
14
+ puts bold("Environment")
15
+ puts " RAILS_ENV: #{ENV.fetch("RAILS_ENV", dim("not set"))}"
16
+ puts " FLARE_KEY: #{key_status}"
17
+ puts " FLARE_URL: #{ENV.fetch("FLARE_URL", dim("https://flare.am (default)"))}"
18
+ puts
19
+
20
+ puts bold("Files")
21
+ puts " Initializer: #{file_status("config/initializers/flare.rb")}"
22
+ puts " .env: #{file_status(".env")}"
23
+ puts " .gitignore: #{file_status(".gitignore")}"
24
+ puts " Database: #{file_status("db/flare.sqlite3")}"
25
+ end
26
+
27
+ private
28
+
29
+ def key_status
30
+ if ENV["FLARE_KEY"] && !ENV["FLARE_KEY"].empty?
31
+ green("set via ENV")
32
+ else
33
+ env_path = File.join(Dir.pwd, ".env")
34
+ if File.exist?(env_path) && File.read(env_path).match?(/^FLARE_KEY=.+/)
35
+ green("set in .env")
36
+ else
37
+ red("not configured")
38
+ end
39
+ end
40
+ end
41
+
42
+ def file_status(relative_path)
43
+ path = File.join(Dir.pwd, relative_path)
44
+ File.exist?(path) ? green("exists") : dim("not found")
45
+ end
46
+ end
47
+ end
data/lib/flare/cli.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "version"
4
+
5
+ module Flare
6
+ module CLI
7
+ COMMANDS = {
8
+ "setup" => "Authenticate and configure Flare for this project",
9
+ "doctor" => "Check your Flare setup for issues",
10
+ "status" => "Show current Flare configuration",
11
+ "version" => "Print the Flare version",
12
+ "help" => "Show this help message",
13
+ }.freeze
14
+
15
+ def self.start(argv)
16
+ command = argv.first
17
+
18
+ case command
19
+ when "setup"
20
+ require_relative "cli/setup_command"
21
+ force = argv.include?("--force")
22
+ SetupCommand.new(force: force).run
23
+ when "doctor"
24
+ require_relative "cli/doctor_command"
25
+ DoctorCommand.new.run
26
+ when "status"
27
+ require_relative "cli/status_command"
28
+ StatusCommand.new.run
29
+ when "version", "-v", "--version"
30
+ puts "flare #{Flare::VERSION}"
31
+ when "help", nil, "-h", "--help"
32
+ print_help
33
+ else
34
+ $stderr.puts "Unknown command: #{command}"
35
+ $stderr.puts
36
+ print_help
37
+ exit 1
38
+ end
39
+ end
40
+
41
+ def self.print_help
42
+ puts "Usage: flare <command>"
43
+ puts
44
+ puts "Commands:"
45
+ COMMANDS.each do |name, description|
46
+ puts " %-12s %s" % [name, description]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "http_metrics_config"
4
+
5
+ module Flare
6
+ class Configuration
7
+ attr_accessor :enabled
8
+ attr_accessor :retention_hours
9
+ attr_accessor :max_spans
10
+ attr_accessor :ignore_request
11
+ attr_writer :database_path
12
+
13
+ # Spans: detailed trace data stored in SQLite (default: development only)
14
+ # Metrics: aggregated counters in memory, flushed periodically (default: production only)
15
+ attr_accessor :spans_enabled
16
+ attr_accessor :metrics_enabled
17
+ attr_accessor :metrics_flush_interval # seconds between flushes (default: 60)
18
+
19
+ # Metrics HTTP submission settings
20
+ attr_accessor :url # URL of the Flare metrics service
21
+ attr_accessor :key # API key for authentication
22
+ attr_accessor :metrics_timeout # HTTP timeout in seconds (default: 5)
23
+ attr_accessor :metrics_gzip # Whether to gzip payloads (default: true)
24
+
25
+ # Default patterns to auto-subscribe to for custom instrumentation
26
+ # Use "app." prefix in your ActiveSupport::Notifications.instrument calls
27
+ DEFAULT_SUBSCRIBE_PATTERNS = %w[app.].freeze
28
+
29
+ attr_accessor :subscribe_patterns
30
+
31
+ # HTTP metrics path tracking configuration.
32
+ # Controls which outgoing HTTP paths are tracked with detail vs collapsed to "*".
33
+ attr_reader :http_metrics_config
34
+
35
+ # Enable debug logging to see what Flare is doing.
36
+ # Set FLARE_DEBUG=1 or configure debug: true in your initializer.
37
+ attr_accessor :debug
38
+
39
+ def initialize
40
+ @enabled = true
41
+ @retention_hours = 24
42
+ @max_spans = 10_000
43
+ @database_path = nil
44
+ @ignore_request = ->(request) { false }
45
+ @subscribe_patterns = DEFAULT_SUBSCRIBE_PATTERNS.dup
46
+ @debug = ENV["FLARE_DEBUG"] == "1"
47
+ @http_metrics_config = HttpMetricsConfig::DEFAULT
48
+
49
+ # Environment-based defaults:
50
+ # - Development: spans ON (detailed debugging), metrics ON
51
+ # - Production: spans OFF (too expensive), metrics ON
52
+ # - Test: spans OFF, metrics OFF
53
+ @spans_enabled = rails_development?
54
+ @metrics_enabled = !rails_test?
55
+ @metrics_flush_interval = 60 # seconds
56
+
57
+ # Metrics HTTP submission defaults
58
+ @url = ENV.fetch("FLARE_URL", credentials_url || "https://flare.am")
59
+ @key = ENV["FLARE_KEY"]
60
+ @metrics_timeout = 5
61
+ @metrics_gzip = true
62
+ end
63
+
64
+ # Check if metrics can be submitted (endpoint and API key configured)
65
+ def metrics_submission_configured?
66
+ !@url.nil? && !@url.empty? &&
67
+ !@key.nil? && !@key.empty?
68
+ end
69
+
70
+ def database_path
71
+ @database_path || default_database_path
72
+ end
73
+
74
+ # Configure HTTP metrics path tracking.
75
+ #
76
+ # config.http_metrics do |http|
77
+ # http.host "api.stripe.com" do |h|
78
+ # h.allow %r{/v1/customers}
79
+ # h.allow %r{/v1/charges}
80
+ # h.map %r{/v1/connect/[\w-]+/transfers}, "/v1/connect/:account/transfers"
81
+ # end
82
+ # http.host "api.github.com", :all
83
+ # end
84
+ def http_metrics(&block)
85
+ # Clone defaults on first customization so user additions merge with built-in defaults
86
+ if @http_metrics_config.equal?(HttpMetricsConfig::DEFAULT)
87
+ @http_metrics_config = @http_metrics_config.dup
88
+ end
89
+ yield @http_metrics_config
90
+ end
91
+
92
+ private
93
+
94
+ def rails_development?
95
+ defined?(Rails) && Rails.env.development?
96
+ rescue StandardError
97
+ false # Default to false for safety - avoids enabling spans unexpectedly in production
98
+ end
99
+
100
+ def rails_test?
101
+ defined?(Rails) && Rails.env.test?
102
+ rescue StandardError
103
+ false
104
+ end
105
+
106
+ def credentials_url
107
+ return nil unless defined?(Rails) && Rails.application&.credentials
108
+ Rails.application.credentials.dig(:flare, :url)
109
+ rescue StandardError
110
+ nil
111
+ end
112
+
113
+ def default_database_path
114
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
115
+ Rails.root.join("db", "flare.sqlite3").to_s
116
+ else
117
+ "flare.sqlite3"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flare
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Flare
6
+
7
+ # Load secrets from Rails credentials if not already set via ENV
8
+ initializer "flare.defaults", before: :load_config_initializers do |app|
9
+ ENV["FLARE_KEY"] ||= app.credentials.dig(:flare, :key)
10
+ end
11
+
12
+ # Serve static assets from the engine's public directory
13
+ initializer "flare.static_assets" do |app|
14
+ app.middleware.use(
15
+ Rack::Static,
16
+ urls: ["/flare-assets"],
17
+ root: root.join("public"),
18
+ cascade: true
19
+ )
20
+ end
21
+
22
+ # Phase 1: Configure OTel SDK and instrumentations before middleware is
23
+ # built so Rack/ActionPack can insert their middleware.
24
+ initializer "flare.opentelemetry", before: :build_middleware_stack do
25
+ Flare.configure_opentelemetry
26
+ end
27
+
28
+ # Phase 2: Start the metrics flusher after all initializers have run
29
+ # so user config (metrics_enabled, flush_interval, etc.) is applied.
30
+ config.after_initialize do
31
+ Flare.start_metrics_flusher
32
+ end
33
+
34
+ # Auto-mount routes in development/test
35
+ initializer "flare.routes", before: :add_routing_paths do |app|
36
+ if Rails.env.development? || Rails.env.test?
37
+ app.routes.prepend do
38
+ mount Flare::Engine => "/flare"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end