flare 0.4.0 → 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 (70) 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 -123
  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 -537
  15. data/test/helper.rb +10 -0
  16. data/test/test_flare.rb +7 -0
  17. metadata +89 -242
  18. checksums.yaml +0 -7
  19. data/CHANGELOG.md +0 -3
  20. data/LICENSE.txt +0 -21
  21. data/README.md +0 -148
  22. data/app/controllers/flare/application_controller.rb +0 -30
  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/client_headers.rb +0 -39
  43. data/lib/flare/deadline.rb +0 -27
  44. data/lib/flare/engine.rb +0 -45
  45. data/lib/flare/filtering_span_processor.rb +0 -429
  46. data/lib/flare/http_metrics_config.rb +0 -101
  47. data/lib/flare/http_transport.rb +0 -76
  48. data/lib/flare/lifecycle.rb +0 -39
  49. data/lib/flare/marker.rb +0 -106
  50. data/lib/flare/metric_counter.rb +0 -51
  51. data/lib/flare/metric_flusher.rb +0 -279
  52. data/lib/flare/metric_key.rb +0 -42
  53. data/lib/flare/metric_span_processor.rb +0 -470
  54. data/lib/flare/metric_storage.rb +0 -67
  55. data/lib/flare/metric_submitter.rb +0 -239
  56. data/lib/flare/recording_batch_span_processor.rb +0 -238
  57. data/lib/flare/rule_manager.rb +0 -141
  58. data/lib/flare/sampler.rb +0 -130
  59. data/lib/flare/source_location.rb +0 -113
  60. data/lib/flare/sqlite_exporter.rb +0 -334
  61. data/lib/flare/storage/sqlite.rb +0 -794
  62. data/lib/flare/storage.rb +0 -54
  63. data/lib/flare/trace_blob.rb +0 -116
  64. data/lib/flare/trace_exporter.rb +0 -182
  65. data/lib/flare/trace_health_reporter.rb +0 -74
  66. data/lib/flare/upload_url_pool.rb +0 -108
  67. data/lib/flare/version.rb +0 -5
  68. data/lib/flare/web_marker_subscriber.rb +0 -76
  69. data/public/flare-assets/flare.css +0 -1245
  70. data/public/flare-assets/images/flipper.png +0 -0
data/lib/flare/marker.rb DELETED
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "concurrent/map"
4
- require "concurrent/atomic/atomic_fixnum"
5
-
6
- module Flare
7
- # Thread-safe registry of trace_ids that Path 2 (the WebMarkerSubscriber)
8
- # has marked for export. FilteringSpanProcessor checks marked? on every
9
- # on_finish; matching spans get forwarded to the trace exporter, the rest
10
- # are dropped.
11
- #
12
- # Each entry records the OWNER span_id (the local rack server span the
13
- # subscriber was inside when it marked the trace). Cleanup is keyed on
14
- # the owner finishing, not the trace root finishing -- remote-parented
15
- # rack spans aren't trace roots, and child spans can outlive their parent
16
- # in OTel, so root-driven cleanup would leak on the dominant production
17
- # case (web app behind a load balancer or service mesh).
18
- #
19
- # Bounded by:
20
- # - sweep(): drops entries older than max_age (default 5 min) so a rack
21
- # span that never finishes (process killed mid-request, exception path
22
- # that skips ensure) doesn't leak forever.
23
- # - hard ceiling at max_entries (default 10k): on overflow, drop oldest
24
- # 10% by marked_at.
25
- class Marker
26
- Entry = Struct.new(:owner_span_id, :rule_id, :marked_at, keyword_init: true)
27
-
28
- DEFAULT_MAX_ENTRIES = 10_000
29
- DEFAULT_MAX_AGE = 5 * 60 # seconds
30
-
31
- attr_reader :evicted_count
32
-
33
- def initialize(max_entries: DEFAULT_MAX_ENTRIES, max_age: DEFAULT_MAX_AGE)
34
- @entries = Concurrent::Map.new
35
- @max_entries = max_entries
36
- @max_age = max_age
37
- @evicted_count = Concurrent::AtomicFixnum.new(0)
38
- end
39
-
40
- def mark(trace_id, owner_span_id:, rule_id:)
41
- @entries[trace_id] = Entry.new(
42
- owner_span_id: owner_span_id,
43
- rule_id: rule_id,
44
- marked_at: monotonic_now
45
- )
46
- maybe_evict_oldest
47
- end
48
-
49
- def marked?(trace_id)
50
- @entries.key?(trace_id)
51
- end
52
-
53
- # True only when span_id matches the marker's owner -- the rack span
54
- # that originally marked this trace. Used by FilteringSpanProcessor to
55
- # decide when to unmark (only when that exact span finishes, not on
56
- # every span that happens to have this trace_id).
57
- def owner?(trace_id, span_id)
58
- entry = @entries[trace_id]
59
- !entry.nil? && entry.owner_span_id == span_id
60
- end
61
-
62
- def rule_id(trace_id)
63
- entry = @entries[trace_id]
64
- entry&.rule_id
65
- end
66
-
67
- def unmark(trace_id)
68
- @entries.delete(trace_id)
69
- end
70
-
71
- def size
72
- @entries.size
73
- end
74
-
75
- # Drop entries older than max_age. Call periodically (the RuleManager's
76
- # scheduler is the natural place) to handle the rack-span-never-finishes
77
- # leak case (CAF-7).
78
- def sweep
79
- threshold = monotonic_now - @max_age
80
- evicted = 0
81
- @entries.each_pair do |trace_id, entry|
82
- if entry.marked_at < threshold
83
- @entries.delete(trace_id)
84
- evicted += 1
85
- end
86
- end
87
- @evicted_count.increment(evicted) if evicted.positive?
88
- evicted
89
- end
90
-
91
- private
92
-
93
- def maybe_evict_oldest
94
- return if @entries.size <= @max_entries
95
-
96
- to_drop = (@max_entries * 0.1).ceil
97
- sorted = @entries.each_pair.to_a.sort_by { |_, entry| entry.marked_at }
98
- sorted.first(to_drop).each { |trace_id, _| @entries.delete(trace_id) }
99
- @evicted_count.increment(to_drop)
100
- end
101
-
102
- def monotonic_now
103
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
- end
105
- end
106
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "concurrent/atomic/atomic_fixnum"
4
-
5
- module Flare
6
- # Thread-safe counter for metric aggregation.
7
- # Uses atomic operations for lock-free increments.
8
- #
9
- # Note: Durations are stored as integer milliseconds. Sub-millisecond
10
- # durations are truncated to 0. For very fast operations (e.g., cache hits),
11
- # the sum_ms may undercount actual time spent.
12
- class MetricCounter
13
- def initialize
14
- @count = Concurrent::AtomicFixnum.new(0)
15
- @sum_ms = Concurrent::AtomicFixnum.new(0)
16
- @error_count = Concurrent::AtomicFixnum.new(0)
17
- end
18
-
19
- def increment(duration_ms:, error: false)
20
- @count.increment
21
- @sum_ms.increment(duration_ms.to_i)
22
- @error_count.increment if error
23
- end
24
-
25
- def add(count:, sum_ms:, error_count: 0)
26
- @count.increment(count.to_i)
27
- @sum_ms.increment(sum_ms.to_i)
28
- @error_count.increment(error_count.to_i)
29
- end
30
-
31
- def count
32
- @count.value
33
- end
34
-
35
- def sum_ms
36
- @sum_ms.value
37
- end
38
-
39
- def error_count
40
- @error_count.value
41
- end
42
-
43
- def to_h
44
- {
45
- count: @count.value,
46
- sum_ms: @sum_ms.value,
47
- error_count: @error_count.value
48
- }
49
- end
50
- end
51
- end
@@ -1,279 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "concurrent/timer_task"
4
- require "concurrent/executor/fixed_thread_pool"
5
- require "opentelemetry/sdk"
6
-
7
- require_relative "deadline"
8
-
9
- module Flare
10
- # Background threads that periodically drain in-memory metrics and submit
11
- # them via HTTP. Uses concurrent-ruby TimerTask + FixedThreadPool, matching
12
- # the pattern in Flipper's telemetry.
13
- #
14
- # Fork-safe: detects forked processes and restarts automatically.
15
- class MetricFlusher
16
- SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
17
- FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
18
- TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
19
- DEFAULT_INTERVAL = 60 # seconds
20
- DEFAULT_SHUTDOWN_TIMEOUT = 5 # seconds
21
-
22
- attr_reader :interval, :shutdown_timeout
23
-
24
- def initialize(storage:, submitter:, interval: DEFAULT_INTERVAL, shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT, health_reporters: [])
25
- @storage = storage
26
- @submitter = submitter
27
- @interval = interval
28
- @shutdown_timeout = shutdown_timeout
29
- @health_reporters = Array(health_reporters)
30
- @pid = $$
31
- @stopped = false
32
- initialize_synchronization
33
- end
34
-
35
- def start
36
- @stopped = false
37
-
38
- @pool = Concurrent::FixedThreadPool.new(1, {
39
- max_queue: 20,
40
- fallback_policy: :discard,
41
- name: "flare-metrics-submit-pool".freeze,
42
- })
43
-
44
- @timer = Concurrent::TimerTask.execute({
45
- execution_interval: @interval,
46
- name: "flare-metrics-drain-timer".freeze,
47
- }) { post_to_pool }
48
- end
49
-
50
- def stop(timeout: @shutdown_timeout)
51
- return if @stopped
52
-
53
- deadline = Deadline.new(timeout)
54
- @stopped = true
55
-
56
- log "Shutting down metrics flusher, draining remaining metrics..."
57
-
58
- if @timer
59
- @timer.shutdown
60
- @timer.wait_for_termination([deadline.remaining || 1, 1].min)
61
- @timer.kill unless @timer.shutdown?
62
- end
63
-
64
- force_flush(timeout: deadline.remaining)
65
-
66
- if @pool
67
- @pool.shutdown
68
- pool_terminated = @pool.wait_for_termination(deadline.remaining || @shutdown_timeout)
69
- @pool.kill unless pool_terminated
70
- end
71
-
72
- log "Metrics flusher stopped"
73
- end
74
-
75
- def restart
76
- @stopped = false
77
- stop
78
- start
79
- end
80
-
81
- # Manually trigger a flush (useful for testing or forced flushes).
82
- def flush_now(timeout: nil)
83
- return 0 unless @storage && @submitter
84
-
85
- detect_forking
86
- count, error, = flush_synchronously(Deadline.new(timeout))
87
- if error
88
- warn "[Flare] Metric submission error: #{error.message}"
89
- end
90
- count
91
- rescue => e
92
- warn "[Flare] Metric flush error: #{e.message}"
93
- 0
94
- end
95
-
96
- def force_flush(timeout: nil)
97
- return SUCCESS unless @storage && @submitter
98
-
99
- detect_forking
100
- deadline = Deadline.new(timeout)
101
- _count, error, timed_out = flush_synchronously(deadline)
102
- return TIMEOUT if timed_out || deadline.expired?
103
- return FAILURE if error
104
-
105
- SUCCESS
106
- rescue => e
107
- warn "[Flare] Metric flush error: #{e.message}"
108
- FAILURE
109
- end
110
-
111
- def running?
112
- @timer&.running? || false
113
- end
114
-
115
- # Re-initialize after fork. Called automatically by MetricSpanProcessor
116
- # on first span in the new process, or manually from Puma/Unicorn
117
- # after_fork hooks.
118
- def after_fork
119
- @pid = $$
120
- @storage.after_fork if @storage.respond_to?(:after_fork)
121
- initialize_synchronization
122
- @timer = nil
123
- @pool = nil
124
- start
125
- end
126
-
127
- private
128
-
129
- def detect_forking
130
- after_fork if @pid != $$
131
- end
132
-
133
- def initialize_synchronization
134
- @submission_mutex = Mutex.new
135
- @submission_condition = ConditionVariable.new
136
- @pending_submissions = 0
137
- @flush_owner = nil
138
- end
139
-
140
- def post_to_pool
141
- return unless reserve_background_submission
142
-
143
- record_health_metrics
144
- drained = @storage.drain
145
- if drained.empty?
146
- log "No metrics to flush"
147
- background_submission_finished
148
- return
149
- end
150
-
151
- log "Drained #{drained.size} metric keys for submission"
152
- posted = @pool.post do
153
- submit_to_cloud(drained)
154
- ensure
155
- background_submission_finished
156
- end
157
- background_submission_finished unless posted
158
- rescue => e
159
- background_submission_finished
160
- warn "[Flare] Metric drain error: #{e.message}"
161
- end
162
-
163
- def submit_to_cloud(drained)
164
- _response, error = @submitter.submit(drained)
165
- if error
166
- warn "[Flare] Metric submission error: #{error.message}"
167
- end
168
- rescue => e
169
- warn "[Flare] Metric submission error: #{e.message}"
170
- end
171
-
172
- def reserve_background_submission
173
- @submission_mutex.synchronize do
174
- return false if @flush_owner || @pending_submissions.positive?
175
-
176
- @pending_submissions += 1
177
- true
178
- end
179
- end
180
-
181
- def background_submission_finished
182
- @submission_mutex.synchronize do
183
- @pending_submissions -= 1 if @pending_submissions.positive?
184
- @submission_condition.broadcast
185
- end
186
- end
187
-
188
- def flush_synchronously(deadline)
189
- return [0, nil, true] unless begin_synchronous_flush(deadline)
190
-
191
- record_health_metrics
192
- drained = @storage.drain
193
- return [0, nil, false] if drained.empty?
194
-
195
- submit_with_deadline(drained, deadline)
196
- ensure
197
- finish_synchronous_flush if @flush_owner == Thread.current
198
- end
199
-
200
- def begin_synchronous_flush(deadline)
201
- @submission_mutex.synchronize do
202
- while @flush_owner && @flush_owner != Thread.current
203
- return false if deadline.expired?
204
-
205
- @submission_condition.wait(@submission_mutex, deadline.remaining)
206
- end
207
- @flush_owner = Thread.current
208
-
209
- while @pending_submissions.positive?
210
- return false if deadline.expired?
211
-
212
- @submission_condition.wait(@submission_mutex, deadline.remaining)
213
- end
214
- end
215
- true
216
- end
217
-
218
- def finish_synchronous_flush
219
- @submission_mutex.synchronize do
220
- @flush_owner = nil
221
- @submission_condition.broadcast
222
- end
223
- end
224
-
225
- def submit_metrics(drained, timeout:)
226
- parameters = @submitter.method(:submit).parameters
227
- accepts_timeout = parameters.any? do |type, name|
228
- type == :keyrest || ([:key, :keyreq].include?(type) && name == :timeout)
229
- end
230
-
231
- if accepts_timeout
232
- @submitter.submit(drained, timeout: timeout)
233
- else
234
- @submitter.submit(drained)
235
- end
236
- end
237
-
238
- def submit_with_deadline(drained, deadline)
239
- operation = { done: false, count: 0, error: nil }
240
- @submission_mutex.synchronize { @pending_submissions += 1 }
241
- Thread.new do
242
- operation[:count], operation[:error] = submit_metrics(drained, timeout: deadline.remaining)
243
- rescue => e
244
- operation[:error] = e
245
- ensure
246
- @submission_mutex.synchronize do
247
- operation[:done] = true
248
- @pending_submissions -= 1
249
- @submission_condition.broadcast
250
- end
251
- end
252
-
253
- @submission_mutex.synchronize do
254
- until operation[:done]
255
- return [0, nil, true] if deadline.expired?
256
-
257
- @submission_condition.wait(@submission_mutex, deadline.remaining)
258
- end
259
- end
260
-
261
- timed_out = deadline.expired? || deadline_error?(operation[:error])
262
- [operation[:count], operation[:error], timed_out]
263
- end
264
-
265
- def deadline_error?(error)
266
- defined?(MetricSubmitter::DeadlineExceeded) && error.is_a?(MetricSubmitter::DeadlineExceeded)
267
- end
268
-
269
- def record_health_metrics
270
- @health_reporters.each { |reporter| reporter.record(@storage) }
271
- rescue => e
272
- warn "[Flare] Health metric recording error: #{e.message}"
273
- end
274
-
275
- def log(message)
276
- Flare.log(message) if Flare.respond_to?(:log)
277
- end
278
- end
279
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Flare
4
- # Identifies a unique metric for aggregation.
5
- # Immutable and hashable for use as Concurrent::Map keys.
6
- class MetricKey
7
- attr_reader :bucket, :namespace, :service, :target, :operation
8
-
9
- def initialize(bucket:, namespace:, service:, target:, operation:)
10
- @bucket = bucket
11
- @namespace = namespace.to_s.freeze
12
- @service = service.to_s.freeze
13
- @target = target&.to_s&.freeze
14
- @operation = operation.to_s.freeze
15
- freeze
16
- end
17
-
18
- def eql?(other)
19
- self.class.eql?(other.class) &&
20
- bucket == other.bucket &&
21
- namespace == other.namespace &&
22
- service == other.service &&
23
- target == other.target &&
24
- operation == other.operation
25
- end
26
- alias == eql?
27
-
28
- def hash
29
- [self.class, bucket, namespace, service, target, operation].hash
30
- end
31
-
32
- def to_h
33
- {
34
- bucket: bucket,
35
- namespace: namespace,
36
- service: service,
37
- target: target,
38
- operation: operation
39
- }
40
- end
41
- end
42
- end