flare 0.4.2 → 1.4.4

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 +111 -0
  9. data/lib/flare/collection.rb +47 -0
  10. data/lib/flare/configuration.rb +64 -123
  11. data/lib/flare/index_builder.rb +29 -0
  12. data/lib/flare/session.rb +150 -0
  13. data/lib/flare/tasks.rb +18 -0
  14. data/lib/flare.rb +27 -524
  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 -78
  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 -281
  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,281 +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
- return if @pid == $$
120
-
121
- @pid = $$
122
- @storage.after_fork if @storage.respond_to?(:after_fork)
123
- initialize_synchronization
124
- @timer = nil
125
- @pool = nil
126
- start
127
- end
128
-
129
- private
130
-
131
- def detect_forking
132
- after_fork if @pid != $$
133
- end
134
-
135
- def initialize_synchronization
136
- @submission_mutex = Mutex.new
137
- @submission_condition = ConditionVariable.new
138
- @pending_submissions = 0
139
- @flush_owner = nil
140
- end
141
-
142
- def post_to_pool
143
- return unless reserve_background_submission
144
-
145
- record_health_metrics
146
- drained = @storage.drain
147
- if drained.empty?
148
- log "No metrics to flush"
149
- background_submission_finished
150
- return
151
- end
152
-
153
- log "Drained #{drained.size} metric keys for submission"
154
- posted = @pool.post do
155
- submit_to_cloud(drained)
156
- ensure
157
- background_submission_finished
158
- end
159
- background_submission_finished unless posted
160
- rescue => e
161
- background_submission_finished
162
- warn "[Flare] Metric drain error: #{e.message}"
163
- end
164
-
165
- def submit_to_cloud(drained)
166
- _response, error = @submitter.submit(drained)
167
- if error
168
- warn "[Flare] Metric submission error: #{error.message}"
169
- end
170
- rescue => e
171
- warn "[Flare] Metric submission error: #{e.message}"
172
- end
173
-
174
- def reserve_background_submission
175
- @submission_mutex.synchronize do
176
- return false if @flush_owner || @pending_submissions.positive?
177
-
178
- @pending_submissions += 1
179
- true
180
- end
181
- end
182
-
183
- def background_submission_finished
184
- @submission_mutex.synchronize do
185
- @pending_submissions -= 1 if @pending_submissions.positive?
186
- @submission_condition.broadcast
187
- end
188
- end
189
-
190
- def flush_synchronously(deadline)
191
- return [0, nil, true] unless begin_synchronous_flush(deadline)
192
-
193
- record_health_metrics
194
- drained = @storage.drain
195
- return [0, nil, false] if drained.empty?
196
-
197
- submit_with_deadline(drained, deadline)
198
- ensure
199
- finish_synchronous_flush if @flush_owner == Thread.current
200
- end
201
-
202
- def begin_synchronous_flush(deadline)
203
- @submission_mutex.synchronize do
204
- while @flush_owner && @flush_owner != Thread.current
205
- return false if deadline.expired?
206
-
207
- @submission_condition.wait(@submission_mutex, deadline.remaining)
208
- end
209
- @flush_owner = Thread.current
210
-
211
- while @pending_submissions.positive?
212
- return false if deadline.expired?
213
-
214
- @submission_condition.wait(@submission_mutex, deadline.remaining)
215
- end
216
- end
217
- true
218
- end
219
-
220
- def finish_synchronous_flush
221
- @submission_mutex.synchronize do
222
- @flush_owner = nil
223
- @submission_condition.broadcast
224
- end
225
- end
226
-
227
- def submit_metrics(drained, timeout:)
228
- parameters = @submitter.method(:submit).parameters
229
- accepts_timeout = parameters.any? do |type, name|
230
- type == :keyrest || ([:key, :keyreq].include?(type) && name == :timeout)
231
- end
232
-
233
- if accepts_timeout
234
- @submitter.submit(drained, timeout: timeout)
235
- else
236
- @submitter.submit(drained)
237
- end
238
- end
239
-
240
- def submit_with_deadline(drained, deadline)
241
- operation = { done: false, count: 0, error: nil }
242
- @submission_mutex.synchronize { @pending_submissions += 1 }
243
- Thread.new do
244
- operation[:count], operation[:error] = submit_metrics(drained, timeout: deadline.remaining)
245
- rescue => e
246
- operation[:error] = e
247
- ensure
248
- @submission_mutex.synchronize do
249
- operation[:done] = true
250
- @pending_submissions -= 1
251
- @submission_condition.broadcast
252
- end
253
- end
254
-
255
- @submission_mutex.synchronize do
256
- until operation[:done]
257
- return [0, nil, true] if deadline.expired?
258
-
259
- @submission_condition.wait(@submission_mutex, deadline.remaining)
260
- end
261
- end
262
-
263
- timed_out = deadline.expired? || deadline_error?(operation[:error])
264
- [operation[:count], operation[:error], timed_out]
265
- end
266
-
267
- def deadline_error?(error)
268
- defined?(MetricSubmitter::DeadlineExceeded) && error.is_a?(MetricSubmitter::DeadlineExceeded)
269
- end
270
-
271
- def record_health_metrics
272
- @health_reporters.each { |reporter| reporter.record(@storage) }
273
- rescue => e
274
- warn "[Flare] Health metric recording error: #{e.message}"
275
- end
276
-
277
- def log(message)
278
- Flare.log(message) if Flare.respond_to?(:log)
279
- end
280
- end
281
- 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