flare 0.1.1 → 1.0.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.

Potentially problematic release.


This version of flare might be problematic. Click here for more details.

Files changed (56) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +55 -0
  6. data/VERSION +1 -0
  7. data/flare.gemspec +66 -0
  8. data/lib/flare/active_record.rb +102 -0
  9. data/lib/flare/collection.rb +47 -0
  10. data/lib/flare/configuration.rb +64 -109
  11. data/lib/flare/index_builder.rb +24 -0
  12. data/lib/flare/session.rb +142 -0
  13. data/lib/flare/tasks.rb +18 -0
  14. data/lib/flare.rb +19 -408
  15. data/test/helper.rb +10 -0
  16. data/test/test_flare.rb +7 -0
  17. metadata +89 -228
  18. checksums.yaml +0 -7
  19. data/CHANGELOG.md +0 -5
  20. data/LICENSE.txt +0 -21
  21. data/README.md +0 -148
  22. data/app/controllers/flare/application_controller.rb +0 -22
  23. data/app/controllers/flare/jobs_controller.rb +0 -55
  24. data/app/controllers/flare/requests_controller.rb +0 -73
  25. data/app/controllers/flare/spans_controller.rb +0 -101
  26. data/app/helpers/flare/application_helper.rb +0 -168
  27. data/app/views/flare/jobs/index.html.erb +0 -69
  28. data/app/views/flare/jobs/show.html.erb +0 -323
  29. data/app/views/flare/requests/index.html.erb +0 -120
  30. data/app/views/flare/requests/show.html.erb +0 -498
  31. data/app/views/flare/spans/index.html.erb +0 -112
  32. data/app/views/flare/spans/show.html.erb +0 -184
  33. data/app/views/layouts/flare/application.html.erb +0 -126
  34. data/config/routes.rb +0 -20
  35. data/exe/flare +0 -9
  36. data/lib/flare/backoff_policy.rb +0 -73
  37. data/lib/flare/cli/doctor_command.rb +0 -129
  38. data/lib/flare/cli/output.rb +0 -45
  39. data/lib/flare/cli/setup_command.rb +0 -404
  40. data/lib/flare/cli/status_command.rb +0 -47
  41. data/lib/flare/cli.rb +0 -50
  42. data/lib/flare/engine.rb +0 -43
  43. data/lib/flare/http_metrics_config.rb +0 -101
  44. data/lib/flare/metric_counter.rb +0 -45
  45. data/lib/flare/metric_flusher.rb +0 -124
  46. data/lib/flare/metric_key.rb +0 -42
  47. data/lib/flare/metric_span_processor.rb +0 -470
  48. data/lib/flare/metric_storage.rb +0 -42
  49. data/lib/flare/metric_submitter.rb +0 -221
  50. data/lib/flare/source_location.rb +0 -113
  51. data/lib/flare/sqlite_exporter.rb +0 -279
  52. data/lib/flare/storage/sqlite.rb +0 -789
  53. data/lib/flare/storage.rb +0 -54
  54. data/lib/flare/version.rb +0 -5
  55. data/public/flare-assets/flare.css +0 -1245
  56. data/public/flare-assets/images/flipper.png +0 -0
@@ -0,0 +1,18 @@
1
+ require 'escape'
2
+
3
+ namespace :flare do
4
+ desc 'Rebuild the Solr index for all searchable models'
5
+ task :rebuild_index => :environment do
6
+ Flare.indexed_models.each do |model|
7
+ puts "Clearing index for #{model.name}..."
8
+ model.clear_solr_index
9
+
10
+ puts "Rebuilding index for #{model.name}..."
11
+ model.rebuild_solr_index
12
+ end
13
+
14
+ puts "Optimizing..."
15
+ Flare.session.commit
16
+ Flare.session.optimize
17
+ end
18
+ end
data/lib/flare.rb CHANGED
@@ -1,415 +1,26 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "flare/version"
4
- require_relative "flare/configuration"
1
+ begin
2
+ # require 'time'
3
+ # require 'date'
4
+ require 'rsolr'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'rsolr'
8
+ end
5
9
 
6
- require "opentelemetry/sdk"
10
+ %w(configuration collection index_builder session active_record).each do |file|
11
+ require File.join(File.dirname(__FILE__), 'flare', file)
12
+ end
7
13
 
8
- require_relative "flare/source_location"
9
- require_relative "flare/metric_key"
10
- require_relative "flare/metric_storage"
11
- require_relative "flare/metric_span_processor"
12
- require_relative "flare/metric_flusher"
13
- require_relative "flare/backoff_policy"
14
- require_relative "flare/metric_submitter"
14
+ ActiveRecord::Base.send(:include, Flare::ActiveRecord)
15
15
 
16
16
  module Flare
17
- class Error < StandardError; end
18
-
19
- MISSING_PARENT_ID = "0000000000000000"
20
- TRANSACTION_NAME_ATTRIBUTE = "flare.transaction_name" unless const_defined?(:TRANSACTION_NAME_ATTRIBUTE)
21
-
22
- module_function
23
-
24
- def configuration
25
- @configuration ||= Configuration.new
26
- end
27
-
28
- def configure
29
- yield(configuration) if block_given?
30
- end
31
-
32
- def enabled?
33
- configuration.enabled
34
- end
35
-
36
- # Set the transaction name for the current span. This overrides the
37
- # default name derived from Rails controller/action or job class.
38
- #
39
- # Useful for Rack middleware, mounted apps, or any request that
40
- # doesn't go through the Rails router.
41
- #
42
- # Flare.transaction_name("RestApi::Routes::Audits#get")
43
- #
44
- def transaction_name(name)
45
- span = OpenTelemetry::Trace.current_span
46
- return unless span.respond_to?(:set_attribute)
47
-
48
- span.set_attribute(TRANSACTION_NAME_ATTRIBUTE, name)
49
- end
50
-
51
- def logger
52
- @logger ||= Logger.new(STDOUT)
53
- end
54
-
55
- def logger=(logger)
56
- @logger = logger
57
- end
58
-
59
- def log(message)
60
- return unless configuration.debug
61
-
62
- logger.info("[Flare] #{message}")
63
- end
64
-
65
- def exporter
66
- @exporter ||= begin
67
- require_relative "flare/sqlite_exporter"
68
- SQLiteExporter.new(configuration.database_path)
69
- rescue LoadError
70
- warn "[Flare] sqlite3 gem not found. Spans are disabled. Add `gem 'sqlite3'` to your Gemfile to enable the development dashboard."
71
- configuration.spans_enabled = false
72
- nil
17
+ class << self
18
+ def session
19
+ @session ||= Flare::Session.new
73
20
  end
74
- end
75
-
76
- def exporter=(exporter)
77
- @exporter = exporter
78
- end
79
-
80
- def span_processor
81
- @span_processor ||= OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
82
- exporter,
83
- max_queue_size: 1000,
84
- max_export_batch_size: 100,
85
- schedule_delay: 1000 # 1 second
86
- )
87
- end
88
-
89
- def span_processor=(span_processor)
90
- @span_processor = span_processor
91
- end
92
-
93
- def tracer
94
- @tracer ||= OpenTelemetry.tracer_provider.tracer("Flare", Flare::VERSION)
95
- end
96
-
97
- def untraced(&block)
98
- OpenTelemetry::Common::Utilities.untraced(&block)
99
- end
100
-
101
- def metric_storage
102
- @metric_storage
103
- end
104
-
105
- def metric_storage=(storage)
106
- @metric_storage = storage
107
- end
108
-
109
- def metric_flusher
110
- @metric_flusher
111
- end
112
-
113
- def metric_flusher=(flusher)
114
- @metric_flusher = flusher
115
- end
116
-
117
- # Manually flush metrics (useful for testing or forced flushes).
118
- def flush_metrics
119
- @metric_flusher&.flush_now || 0
120
- end
121
-
122
- # Re-initialize metric flusher after fork.
123
- # Call this from Puma/Unicorn after_fork hooks.
124
- def after_fork
125
- @metric_flusher&.after_fork
126
- end
127
-
128
- # Configure OpenTelemetry SDK and instrumentations. Must run before the
129
- # middleware stack is built so Rack/ActionPack can insert their middleware.
130
- # Note: metrics flusher is started separately via start_metrics_flusher
131
- # after user initializers have run.
132
- def configure_opentelemetry
133
- return if @otel_configured
134
-
135
- # Suppress noisy OTel INFO logs
136
- OpenTelemetry.logger = Logger.new(STDOUT, level: Logger::WARN)
137
-
138
- service_name = if defined?(Rails) && Rails.application
139
- Rails.application.class.module_parent_name.underscore rescue "rails_app"
140
- else
141
- "app"
142
- end
143
-
144
- # Require only the instrumentations we want
145
- require "opentelemetry-instrumentation-rack"
146
- require "opentelemetry-instrumentation-net_http"
147
- require "opentelemetry-instrumentation-active_support"
148
- require "opentelemetry/instrumentation/active_support/span_subscriber"
149
- require "opentelemetry-instrumentation-action_pack" if defined?(ActionController)
150
- require "opentelemetry-instrumentation-action_view" if defined?(ActionView)
151
- require "opentelemetry-instrumentation-active_job" if defined?(ActiveJob)
152
-
153
- # Tell the SDK not to try configuring OTLP from env vars.
154
- # Flare manages its own exporters (SQLite for spans, HTTP for metrics).
155
- ENV["OTEL_TRACES_EXPORTER"] ||= "none"
156
-
157
- log "Configuring OpenTelemetry (service=#{service_name})"
158
-
159
- OpenTelemetry::SDK.configure do |c|
160
- c.service_name = service_name
161
-
162
- # Spans: detailed trace data stored in SQLite
163
- if configuration.spans_enabled && exporter
164
- c.add_span_processor(span_processor)
165
- log "Spans enabled (database=#{configuration.database_path})"
166
- end
167
-
168
- # Configure specific instrumentations
169
- c.use "OpenTelemetry::Instrumentation::Rack",
170
- untraced_requests: ->(env) {
171
- request = Rack::Request.new(env)
172
- return true if request.path.start_with?("/flare")
173
-
174
- configuration.ignore_request.call(request)
175
- }
176
- c.use "OpenTelemetry::Instrumentation::Net::HTTP"
177
- c.use "OpenTelemetry::Instrumentation::ActiveSupport"
178
- c.use "OpenTelemetry::Instrumentation::ActionPack" if defined?(ActionController)
179
- c.use "OpenTelemetry::Instrumentation::ActionView" if defined?(ActionView)
180
- c.use "OpenTelemetry::Instrumentation::ActiveJob" if defined?(ActiveJob)
181
- end
182
-
183
- # Subscribe to common ActiveSupport notification patterns
184
- # This captures SQL, cache, mailer, and custom notifications.
185
- # Required for both spans (detailed traces) and metrics (aggregated counters)
186
- # because DB, cache, and mailer data flows through ActiveSupport notifications.
187
- if configuration.spans_enabled || configuration.metrics_enabled
188
- subscribe_to_notifications
189
- end
190
-
191
- at_exit do
192
- log "Shutting down..."
193
- if configuration.spans_enabled && @span_processor
194
- span_processor.force_flush
195
- span_processor.shutdown
196
- log "Span processor flushed and stopped"
197
- end
198
- log "Shutdown complete"
199
- end
200
-
201
- @otel_configured = true
202
- end
203
-
204
- # Start the metrics flusher. Called from config.after_initialize so
205
- # user configuration (metrics_enabled, flush_interval, etc.) is applied.
206
- def start_metrics_flusher
207
- return unless configuration.metrics_enabled
208
-
209
- @metric_storage ||= MetricStorage.new
210
- metric_processor = MetricSpanProcessor.new(
211
- storage: @metric_storage,
212
- http_metrics_config: configuration.http_metrics_config
213
- )
214
- OpenTelemetry.tracer_provider.add_span_processor(metric_processor)
215
-
216
- log "Metrics enabled (endpoint=#{configuration.url} key=#{configuration.key ? 'present' : 'missing'})"
217
-
218
- if configuration.metrics_submission_configured?
219
- submitter = MetricSubmitter.new(
220
- endpoint: configuration.url,
221
- api_key: configuration.key
222
- )
223
- @metric_flusher = MetricFlusher.new(
224
- storage: @metric_storage,
225
- submitter: submitter,
226
- interval: configuration.metrics_flush_interval
227
- )
228
- @metric_flusher.start
229
- log "Metrics flusher started (interval=#{configuration.metrics_flush_interval}s)"
230
-
231
- at_exit { @metric_flusher&.stop }
232
- else
233
- log "Metrics submission not configured (missing url or key)"
234
- end
235
- end
236
-
237
- # Payload transformers for different notification types
238
- NOTIFICATION_TRANSFORMERS = {
239
- "sql.active_record" => ->(payload) {
240
- attrs = {}
241
- attrs["db.system"] = payload[:connection]&.adapter_name&.downcase rescue nil
242
- attrs["db.statement"] = payload[:sql] if payload[:sql]
243
- attrs["name"] = payload[:name] if payload[:name]
244
- attrs["db.name"] = payload[:connection]&.pool&.db_config&.name rescue nil
245
- # Capture source location (app code that triggered this query)
246
- SourceLocation.add_to_attributes(attrs)
247
- attrs
248
- },
249
- "instantiation.active_record" => ->(payload) {
250
- attrs = {}
251
- attrs["record_count"] = payload[:record_count] if payload[:record_count]
252
- attrs["class_name"] = payload[:class_name] if payload[:class_name]
253
- attrs
254
- },
255
- "cache_read.active_support" => ->(payload) {
256
- store = payload[:store]
257
- store_name = store.is_a?(String) ? store : store&.class&.name
258
- { "key" => payload[:key]&.to_s, "hit" => payload[:hit], "store" => store_name }
259
- },
260
- "cache_write.active_support" => ->(payload) {
261
- store = payload[:store]
262
- store_name = store.is_a?(String) ? store : store&.class&.name
263
- { "key" => payload[:key]&.to_s, "store" => store_name }
264
- },
265
- "cache_delete.active_support" => ->(payload) {
266
- store = payload[:store]
267
- store_name = store.is_a?(String) ? store : store&.class&.name
268
- { "key" => payload[:key]&.to_s, "store" => store_name }
269
- },
270
- "cache_exist?.active_support" => ->(payload) {
271
- store = payload[:store]
272
- store_name = store.is_a?(String) ? store : store&.class&.name
273
- { "key" => payload[:key]&.to_s, "exist" => payload[:exist], "store" => store_name }
274
- },
275
- "cache_fetch_hit.active_support" => ->(payload) {
276
- store = payload[:store]
277
- store_name = store.is_a?(String) ? store : store&.class&.name
278
- { "key" => payload[:key]&.to_s, "store" => store_name }
279
- },
280
- "deliver.action_mailer" => ->(payload) {
281
- attrs = {}
282
- attrs["mailer"] = payload[:mailer] if payload[:mailer]
283
- attrs["message_id"] = payload[:message_id] if payload[:message_id]
284
- attrs["to"] = Array(payload[:to]).join(", ") if payload[:to]
285
- attrs["subject"] = payload[:subject] if payload[:subject]
286
- attrs
287
- },
288
- "process.action_mailer" => ->(payload) {
289
- attrs = {}
290
- attrs["mailer"] = payload[:mailer] if payload[:mailer]
291
- attrs["action"] = payload[:action] if payload[:action]
292
- attrs
293
- }
294
- }.freeze
295
-
296
- def subscribe_to_notifications
297
- NOTIFICATION_TRANSFORMERS.each do |pattern, transformer|
298
- OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
299
- rescue
300
- # Ignore errors for patterns that don't exist
301
- end
302
-
303
- # Auto-subscribe to custom patterns (default: "app.*")
304
- # This lets users just do: ActiveSupport::Notifications.instrument("app.whatever") { }
305
- subscribe_to_custom_patterns
306
- end
307
-
308
- def subscribe_to_custom_patterns
309
- configuration.subscribe_patterns.each do |prefix|
310
- # Subscribe to all notifications starting with this prefix
311
- pattern = /\A#{Regexp.escape(prefix)}/
312
- default_transformer = ->(payload) {
313
- attrs = payload.transform_keys(&:to_s).select { |_, v|
314
- v.is_a?(String) || v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass)
315
- }
316
- SourceLocation.add_to_attributes(attrs)
317
- attrs
318
- }
319
- OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, default_transformer)
21
+
22
+ def indexed_models
23
+ @@indexed_models ||= []
320
24
  end
321
25
  end
322
-
323
- # Subscribe to any ActiveSupport::Notification and create spans for it
324
- #
325
- # @param pattern [String, Regexp] The notification pattern to subscribe to
326
- # @param transformer [Proc, nil] Optional proc to transform payload into span attributes
327
- # If nil, all payload keys become span attributes
328
- #
329
- # @example Subscribe to a custom notification
330
- # Flare.subscribe("my_service.call")
331
- #
332
- # @example Subscribe with custom attribute transformer
333
- # Flare.subscribe("stripe.charge") do |payload|
334
- # { "charge_id" => payload[:id], "amount" => payload[:amount] }
335
- # end
336
- #
337
- def subscribe(pattern, &transformer)
338
- transformer ||= ->(payload) {
339
- # Default: convert all payload keys to string attributes
340
- payload.transform_keys(&:to_s).transform_values(&:to_s)
341
- }
342
- OpenTelemetry::Instrumentation::ActiveSupport.subscribe(tracer, pattern, transformer)
343
- end
344
-
345
- # Instrument a block of code, creating a span that shows up in Flare
346
- #
347
- # NOTE: This method only works when Flare is loaded (typically development).
348
- # For instrumentation that works in all environments, use ActiveSupport::Notifications
349
- # directly and subscribe with Flare.subscribe in your initializer.
350
- #
351
- # @param name [String] The name of the span (e.g., "my_service.call", "external_api.fetch")
352
- # @param attributes [Hash] Optional attributes to add to the span
353
- # @yield The block to instrument
354
- # @return The return value of the block
355
- #
356
- # @example Basic usage (dev only)
357
- # Flare.instrument("geocoding.lookup") do
358
- # geocoder.lookup(address)
359
- # end
360
- #
361
- # @example For all environments, use ActiveSupport::Notifications instead:
362
- # # In your app code (works everywhere):
363
- # ActiveSupport::Notifications.instrument("myapp.geocoding", address: addr) do
364
- # geocoder.lookup(addr)
365
- # end
366
- #
367
- # # In config/initializers/flare.rb (only loaded in dev):
368
- # Flare.subscribe("myapp.geocoding")
369
- #
370
- def instrument(name, attributes = {}, &block)
371
- return yield unless enabled?
372
-
373
- # Add source location
374
- location = SourceLocation.find
375
- if location
376
- attributes["code.filepath"] = location[:filepath]
377
- attributes["code.lineno"] = location[:lineno]
378
- attributes["code.function"] = location[:function] if location[:function]
379
- end
380
-
381
- tracer.in_span(name, attributes: attributes, kind: :internal) do |span|
382
- yield span
383
- end
384
- end
385
-
386
- def storage
387
- @storage ||= begin
388
- require_relative "flare/storage/sqlite"
389
- Storage::SQLite.new(configuration.database_path)
390
- rescue LoadError
391
- warn "[Flare] sqlite3 gem not found. Dashboard is disabled. Add `gem 'sqlite3'` to your Gemfile to enable it."
392
- configuration.spans_enabled = false
393
- nil
394
- end
395
- end
396
-
397
- def reset_storage!
398
- @storage = nil
399
- end
400
-
401
- def reset!
402
- @configuration = nil
403
- @exporter = nil
404
- @span_processor = nil
405
- @tracer = nil
406
- @storage = nil
407
- @metric_flusher&.stop
408
- @metric_flusher = nil
409
- @metric_storage = nil
410
- @otel_configured = false
411
- end
412
- end
413
-
414
- require_relative "flare/storage"
415
- require_relative "flare/engine" if defined?(Rails)
26
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'flare'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestFlare < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end