flare 0.3.0 → 0.4.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.
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "concurrent/timer_task"
4
4
  require "concurrent/executor/fixed_thread_pool"
5
+ require "opentelemetry/sdk"
6
+
7
+ require_relative "deadline"
5
8
 
6
9
  module Flare
7
10
  # Background threads that periodically drain in-memory metrics and submit
@@ -10,6 +13,9 @@ module Flare
10
13
  #
11
14
  # Fork-safe: detects forked processes and restarts automatically.
12
15
  class MetricFlusher
16
+ SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
17
+ FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
18
+ TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
13
19
  DEFAULT_INTERVAL = 60 # seconds
14
20
  DEFAULT_SHUTDOWN_TIMEOUT = 5 # seconds
15
21
 
@@ -23,6 +29,7 @@ module Flare
23
29
  @health_reporters = Array(health_reporters)
24
30
  @pid = $$
25
31
  @stopped = false
32
+ initialize_synchronization
26
33
  end
27
34
 
28
35
  def start
@@ -40,23 +47,25 @@ module Flare
40
47
  }) { post_to_pool }
41
48
  end
42
49
 
43
- def stop
50
+ def stop(timeout: @shutdown_timeout)
44
51
  return if @stopped
45
52
 
53
+ deadline = Deadline.new(timeout)
46
54
  @stopped = true
47
55
 
48
56
  log "Shutting down metrics flusher, draining remaining metrics..."
49
57
 
50
58
  if @timer
51
59
  @timer.shutdown
52
- @timer.wait_for_termination(1)
60
+ @timer.wait_for_termination([deadline.remaining || 1, 1].min)
53
61
  @timer.kill unless @timer.shutdown?
54
62
  end
55
63
 
64
+ force_flush(timeout: deadline.remaining)
65
+
56
66
  if @pool
57
- post_to_pool # one last drain
58
67
  @pool.shutdown
59
- pool_terminated = @pool.wait_for_termination(@shutdown_timeout)
68
+ pool_terminated = @pool.wait_for_termination(deadline.remaining || @shutdown_timeout)
60
69
  @pool.kill unless pool_terminated
61
70
  end
62
71
 
@@ -70,14 +79,11 @@ module Flare
70
79
  end
71
80
 
72
81
  # Manually trigger a flush (useful for testing or forced flushes).
73
- def flush_now
82
+ def flush_now(timeout: nil)
74
83
  return 0 unless @storage && @submitter
75
84
 
76
- record_health_metrics
77
- drained = @storage.drain
78
- return 0 if drained.empty?
79
-
80
- count, error = @submitter.submit(drained)
85
+ detect_forking
86
+ count, error, = flush_synchronously(Deadline.new(timeout))
81
87
  if error
82
88
  warn "[Flare] Metric submission error: #{error.message}"
83
89
  end
@@ -87,6 +93,21 @@ module Flare
87
93
  0
88
94
  end
89
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
+
90
111
  def running?
91
112
  @timer&.running? || false
92
113
  end
@@ -96,22 +117,46 @@ module Flare
96
117
  # after_fork hooks.
97
118
  def after_fork
98
119
  @pid = $$
99
- restart
120
+ @storage.after_fork if @storage.respond_to?(:after_fork)
121
+ initialize_synchronization
122
+ @timer = nil
123
+ @pool = nil
124
+ start
100
125
  end
101
126
 
102
127
  private
103
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
+
104
140
  def post_to_pool
141
+ return unless reserve_background_submission
142
+
105
143
  record_health_metrics
106
144
  drained = @storage.drain
107
145
  if drained.empty?
108
146
  log "No metrics to flush"
147
+ background_submission_finished
109
148
  return
110
149
  end
111
150
 
112
151
  log "Drained #{drained.size} metric keys for submission"
113
- @pool.post { submit_to_cloud(drained) }
152
+ posted = @pool.post do
153
+ submit_to_cloud(drained)
154
+ ensure
155
+ background_submission_finished
156
+ end
157
+ background_submission_finished unless posted
114
158
  rescue => e
159
+ background_submission_finished
115
160
  warn "[Flare] Metric drain error: #{e.message}"
116
161
  end
117
162
 
@@ -124,6 +169,103 @@ module Flare
124
169
  warn "[Flare] Metric submission error: #{e.message}"
125
170
  end
126
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
+
127
269
  def record_health_metrics
128
270
  @health_reporters.each { |reporter| reporter.record(@storage) }
129
271
  rescue => e
@@ -9,14 +9,17 @@ module Flare
9
9
  class MetricStorage
10
10
  def initialize
11
11
  @storage = Concurrent::Map.new
12
+ @pid = $$
12
13
  end
13
14
 
14
15
  def increment(key, duration_ms:, error: false)
16
+ detect_forking
15
17
  counter = @storage.compute_if_absent(key) { MetricCounter.new }
16
18
  counter.increment(duration_ms: duration_ms, error: error)
17
19
  end
18
20
 
19
21
  def add(key, count:, sum_ms:, error_count: 0)
22
+ detect_forking
20
23
  counter = @storage.compute_if_absent(key) { MetricCounter.new }
21
24
  counter.add(count: count, sum_ms: sum_ms, error_count: error_count)
22
25
  end
@@ -24,6 +27,7 @@ module Flare
24
27
  # Atomically retrieves and clears all metrics.
25
28
  # Returns a frozen hash of MetricKey => counter data.
26
29
  def drain
30
+ detect_forking
27
31
  result = {}
28
32
  @storage.keys.each do |key|
29
33
  counter = @storage.delete(key)
@@ -33,15 +37,31 @@ module Flare
33
37
  end
34
38
 
35
39
  def size
40
+ detect_forking
36
41
  @storage.size
37
42
  end
38
43
 
39
44
  def empty?
45
+ detect_forking
40
46
  @storage.empty?
41
47
  end
42
48
 
43
49
  def [](key)
50
+ detect_forking
44
51
  @storage[key]
45
52
  end
53
+
54
+ def after_fork
55
+ return if @pid == $$
56
+
57
+ @pid = $$
58
+ @storage = Concurrent::Map.new
59
+ end
60
+
61
+ private
62
+
63
+ def detect_forking
64
+ after_fork
65
+ end
46
66
  end
47
67
  end
@@ -5,7 +5,9 @@ require "json"
5
5
  require "zlib"
6
6
  require "stringio"
7
7
  require "securerandom"
8
- require "socket"
8
+
9
+ require_relative "client_headers"
10
+ require_relative "deadline"
9
11
 
10
12
  module Flare
11
13
  # Submits metrics to the Flare metrics service via HTTP.
@@ -13,7 +15,6 @@ module Flare
13
15
  class MetricSubmitter
14
16
  SCHEMA_VERSION = "V1"
15
17
  GZIP_ENCODING = "gzip"
16
- USER_AGENT = "Flare Ruby/#{Flare::VERSION}"
17
18
 
18
19
  # Default timeouts (in seconds)
19
20
  DEFAULT_OPEN_TIMEOUT = 2
@@ -45,6 +46,8 @@ module Flare
45
46
  end
46
47
  end
47
48
 
49
+ class DeadlineExceeded < StandardError; end
50
+
48
51
  attr_reader :endpoint, :api_key, :backoff_policy
49
52
 
50
53
  def initialize(endpoint:, api_key:, project: nil, environment: nil, backoff_policy: nil, open_timeout: nil, read_timeout: nil, write_timeout: nil)
@@ -60,23 +63,28 @@ module Flare
60
63
 
61
64
  # Submit drained metrics to the server.
62
65
  # Returns [success_count, error] where error may be nil on success.
63
- def submit(drained)
66
+ def submit(drained, timeout: nil)
64
67
  return [0, nil] if drained.empty?
65
68
 
69
+ deadline = Deadline.new(timeout)
70
+
66
71
  request_id = SecureRandom.uuid
67
- Flare.log "Submitting #{drained.size} metrics to #{@endpoint} (request_id=#{request_id})"
72
+ Flare.log "Submitting #{drained.size} metrics to #{@endpoint} (request_id=#{request_id})" if Flare.respond_to?(:log)
68
73
 
69
74
  body = build_body(drained, request_id)
70
75
  return [0, nil] if body.nil?
76
+ return [0, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
71
77
 
72
78
  @backoff_policy.reset
73
- response, error = retry_with_backoff(MAX_RETRIES) { post(body, request_id) }
79
+ response, error = retry_with_backoff(MAX_RETRIES, deadline: deadline) do
80
+ post(body, request_id, timeout: deadline.remaining)
81
+ end
74
82
 
75
83
  if error
76
- Flare.log "Submission failed: #{error.message} (request_id=#{request_id})"
84
+ Flare.log "Submission failed: #{error.message} (request_id=#{request_id})" if Flare.respond_to?(:log)
77
85
  [0, error]
78
86
  else
79
- Flare.log "Submission succeeded: #{response.code} (request_id=#{request_id})"
87
+ Flare.log "Submission succeeded: #{response.code} (request_id=#{request_id})" if Flare.respond_to?(:log)
80
88
  [drained.size, nil]
81
89
  end
82
90
  end
@@ -111,28 +119,25 @@ module Flare
111
119
  nil
112
120
  end
113
121
 
114
- def post(body, request_id)
122
+ def post(body, request_id, timeout: nil)
123
+ raise DeadlineExceeded, "metric submission deadline exceeded" if timeout == 0
124
+
115
125
  http = Net::HTTP.new(@endpoint.host, @endpoint.port)
116
126
  http.use_ssl = @endpoint.scheme == "https"
117
- http.open_timeout = @open_timeout
118
- http.read_timeout = @read_timeout
119
- http.write_timeout = @write_timeout if http.respond_to?(:write_timeout=)
127
+ http.open_timeout = effective_timeout(@open_timeout, timeout)
128
+ http.read_timeout = effective_timeout(@read_timeout, timeout)
129
+ http.write_timeout = effective_timeout(@write_timeout, timeout) if http.respond_to?(:write_timeout=)
120
130
 
121
131
  request_uri = @endpoint.request_uri
122
132
  request = Net::HTTP::Post.new(request_uri == "" ? "/" : request_uri)
123
133
  request["Content-Type"] = "application/json"
124
134
  request["Content-Encoding"] = GZIP_ENCODING
125
135
  request["Authorization"] = "Bearer #{@api_key}"
126
- request["User-Agent"] = USER_AGENT
127
136
  request["X-Request-Id"] = request_id
128
137
  request["X-Schema-Version"] = SCHEMA_VERSION
129
138
 
130
- # Client metadata headers (like Flipper)
131
- request["X-Client-Language"] = "ruby"
132
- request["X-Client-Language-Version"] = RUBY_VERSION
133
- request["X-Client-Platform"] = RUBY_PLATFORM
134
- request["X-Client-Pid"] = Process.pid.to_s
135
- request["X-Client-Hostname"] = Socket.gethostname rescue "unknown"
139
+ # Client + version identifying headers, shared across every Flare-API request.
140
+ ClientHeaders.to_h.each { |name, value| request[name] = value }
136
141
 
137
142
  request.body = body
138
143
  response = http.request(request)
@@ -163,20 +168,27 @@ module Flare
163
168
  )
164
169
  end
165
170
 
166
- def retry_with_backoff(max_attempts)
171
+ def retry_with_backoff(max_attempts, deadline:)
167
172
  attempts_remaining = max_attempts
168
173
  last_error = nil
169
174
 
170
175
  while attempts_remaining > 0
171
176
  begin
177
+ return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
178
+
172
179
  result, should_retry = yield
180
+ return [nil, DeadlineExceeded.new("metric submission deadline exceeded")] if deadline.expired?
173
181
  return [result, nil] unless should_retry
174
- rescue SubmissionError, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
182
+ rescue SubmissionError, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET => e
175
183
  last_error = e
176
184
  attempts_remaining -= 1
177
185
 
178
186
  if attempts_remaining > 0
179
187
  sleep_time = @backoff_policy.next_interval / 1000.0
188
+ remaining = deadline.remaining
189
+ if remaining && sleep_time >= remaining
190
+ return [nil, DeadlineExceeded.new("metric submission deadline exceeded")]
191
+ end
180
192
  sleep(sleep_time)
181
193
  end
182
194
  next
@@ -189,6 +201,12 @@ module Flare
189
201
  [nil, last_error]
190
202
  end
191
203
 
204
+ def effective_timeout(configured_timeout, remaining)
205
+ return configured_timeout unless remaining
206
+
207
+ [configured_timeout, remaining].min
208
+ end
209
+
192
210
  def gzip(string)
193
211
  io = StringIO.new
194
212
  io.set_encoding("BINARY")
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "opentelemetry/sdk"
5
+
6
+ require_relative "deadline"
7
+
8
+ module Flare
9
+ # An asynchronous, bounded span processor that exports every ended recording
10
+ # span. OpenTelemetry's BatchSpanProcessor only accepts sampled spans, which
11
+ # excludes RECORD_ONLY spans needed by Flare's local development dashboard.
12
+ class RecordingBatchSpanProcessor
13
+ SUCCESS = OpenTelemetry::SDK::Trace::Export::SUCCESS
14
+ FAILURE = OpenTelemetry::SDK::Trace::Export::FAILURE
15
+ TIMEOUT = OpenTelemetry::SDK::Trace::Export::TIMEOUT
16
+
17
+ def initialize(exporter, exporter_timeout: 30_000, schedule_delay: 5_000,
18
+ max_queue_size: 2_048, max_export_batch_size: 512, logger: nil)
19
+ raise ArgumentError if max_export_batch_size > max_queue_size
20
+
21
+ @exporter = exporter
22
+ @exporter_timeout = exporter_timeout / 1_000.0
23
+ @schedule_delay = schedule_delay / 1_000.0
24
+ @max_queue_size = max_queue_size
25
+ @max_export_batch_size = max_export_batch_size
26
+ @logger = logger || Logger.new($stderr, level: Logger::WARN)
27
+ @pid = $$
28
+ initialize_synchronization
29
+ start_worker
30
+ end
31
+
32
+ def on_start(_span, _parent_context); end
33
+
34
+ def on_finish(span)
35
+ detect_forking
36
+
37
+ @mutex.synchronize do
38
+ overflow = @queue.length + 1 - @max_queue_size
39
+ @queue.shift(overflow) if overflow.positive?
40
+ @queue << span
41
+ @condition.signal if @queue.length >= @max_export_batch_size
42
+ end
43
+ end
44
+
45
+ def force_flush(timeout: nil)
46
+ detect_forking
47
+ deadline = Deadline.new(timeout)
48
+ return TIMEOUT unless begin_flush(deadline)
49
+
50
+ snapshot = snapshot_for_flush
51
+ operation = start_flush_export(snapshot, deadline)
52
+ result = wait_for_flush_export(operation, deadline)
53
+ [@flush_prior_result, result].max
54
+ rescue StandardError => e
55
+ log_export_error(e)
56
+ FAILURE
57
+ ensure
58
+ finish_flush if @flush_owner == Thread.current
59
+ end
60
+
61
+ def shutdown(timeout: nil)
62
+ detect_forking
63
+ deadline = Deadline.new(timeout)
64
+
65
+ worker = @mutex.synchronize do
66
+ @stopped = true
67
+ @condition.broadcast
68
+ @worker
69
+ end
70
+ worker&.join(deadline.remaining)
71
+ return TIMEOUT if worker&.alive? || deadline.expired?
72
+
73
+ result = force_flush(timeout: deadline.remaining)
74
+ return result unless result == SUCCESS
75
+ return TIMEOUT if deadline.expired?
76
+
77
+ exporter_result = @exporter.shutdown(timeout: deadline.remaining)
78
+ deadline.expired? ? TIMEOUT : exporter_result
79
+ rescue StandardError => e
80
+ log_export_error(e)
81
+ FAILURE
82
+ end
83
+
84
+ private
85
+
86
+ def initialize_synchronization
87
+ @mutex = Mutex.new
88
+ @condition = ConditionVariable.new
89
+ @queue = []
90
+ @active_exports = 0
91
+ @export_completion_sequence = 0
92
+ @last_export_result = SUCCESS
93
+ @flush_prior_result = SUCCESS
94
+ @flush_owner = nil
95
+ @stopped = false
96
+ @worker = nil
97
+ end
98
+
99
+ def worker_loop
100
+ loop do
101
+ batch = @mutex.synchronize do
102
+ while !@stopped && (@queue.empty? || @flush_owner || @active_exports.positive?)
103
+ @condition.wait(@mutex, @schedule_delay)
104
+ break if !@queue.empty? && !@flush_owner && @active_exports.zero?
105
+ end
106
+ return if @stopped
107
+
108
+ @active_exports += 1
109
+ @queue.shift(@max_export_batch_size)
110
+ end
111
+
112
+ result = export_batch(batch, timeout: @exporter_timeout)
113
+ ensure
114
+ export_finished(result || FAILURE) if batch
115
+ end
116
+ end
117
+
118
+ def begin_flush(deadline)
119
+ @mutex.synchronize do
120
+ initial_sequence = @export_completion_sequence
121
+ while @flush_owner && @flush_owner != Thread.current
122
+ return false if deadline.expired?
123
+
124
+ @condition.wait(@mutex, deadline.remaining)
125
+ end
126
+ @flush_owner = Thread.current
127
+
128
+ while @active_exports.positive?
129
+ return false if deadline.expired?
130
+
131
+ @condition.wait(@mutex, deadline.remaining)
132
+ end
133
+ @flush_prior_result = if @export_completion_sequence > initial_sequence
134
+ @last_export_result
135
+ else
136
+ SUCCESS
137
+ end
138
+ end
139
+ true
140
+ end
141
+
142
+ def finish_flush
143
+ @mutex.synchronize do
144
+ @flush_owner = nil
145
+ @condition.broadcast
146
+ end
147
+ end
148
+
149
+ def snapshot_for_flush
150
+ @mutex.synchronize { @queue.shift(@queue.length) }
151
+ end
152
+
153
+ def export_snapshot(snapshot, deadline)
154
+ until snapshot.empty?
155
+ return TIMEOUT if deadline.expired?
156
+
157
+ batch = snapshot.shift(@max_export_batch_size)
158
+ result = export_batch(batch, timeout: deadline.remaining)
159
+ return result unless result == SUCCESS
160
+ end
161
+ SUCCESS
162
+ ensure
163
+ @mutex.synchronize { @queue.unshift(*snapshot) } if snapshot&.any?
164
+ end
165
+
166
+ def start_flush_export(snapshot, deadline)
167
+ operation = { done: false, result: nil }
168
+ @mutex.synchronize { @active_exports += 1 }
169
+ Thread.new do
170
+ result = export_snapshot(snapshot, deadline)
171
+ if result == SUCCESS && !deadline.expired?
172
+ result = @exporter.force_flush(timeout: deadline.remaining)
173
+ end
174
+ operation[:result] = deadline.expired? ? TIMEOUT : result
175
+ rescue StandardError => e
176
+ log_export_error(e)
177
+ operation[:result] = FAILURE
178
+ ensure
179
+ @mutex.synchronize do
180
+ operation[:done] = true
181
+ complete_export(operation[:result])
182
+ end
183
+ end
184
+ operation
185
+ end
186
+
187
+ def wait_for_flush_export(operation, deadline)
188
+ @mutex.synchronize do
189
+ until operation[:done]
190
+ return TIMEOUT if deadline.expired?
191
+
192
+ @condition.wait(@mutex, deadline.remaining)
193
+ end
194
+ end
195
+ operation[:result]
196
+ end
197
+
198
+ def export_batch(spans, timeout:)
199
+ span_data = spans.map { |span| span.respond_to?(:to_span_data) ? span.to_span_data : span }
200
+ @exporter.export(span_data, timeout: timeout)
201
+ rescue StandardError => e
202
+ log_export_error(e)
203
+ FAILURE
204
+ end
205
+
206
+ def export_finished(result)
207
+ @mutex.synchronize do
208
+ complete_export(result)
209
+ end
210
+ end
211
+
212
+ def complete_export(result)
213
+ @active_exports -= 1
214
+ @export_completion_sequence += 1
215
+ @last_export_result = result || FAILURE
216
+ @condition.broadcast
217
+ end
218
+
219
+ def detect_forking
220
+ return if @pid == $$
221
+
222
+ # Only the forking thread survives. Replacing synchronization objects
223
+ # avoids waiting on locks or in-flight state owned by vanished threads.
224
+ @pid = $$
225
+ initialize_synchronization
226
+ start_worker
227
+ end
228
+
229
+ def start_worker
230
+ @worker = Thread.new { worker_loop }
231
+ @worker.name = "flare-recording-batch-span-processor"
232
+ end
233
+
234
+ def log_export_error(error)
235
+ @logger.warn("[Flare::RecordingBatchSpanProcessor] export failed: #{error.class}: #{error.message}")
236
+ end
237
+ end
238
+ end
@@ -5,6 +5,7 @@ require "logger"
5
5
  require "concurrent/timer_task"
6
6
  require "concurrent/atomic/atomic_fixnum"
7
7
 
8
+ require_relative "client_headers"
8
9
  require_relative "http_transport"
9
10
 
10
11
  module Flare
@@ -118,11 +119,11 @@ module Flare
118
119
  end
119
120
 
120
121
  def request_headers
121
- headers = {
122
+ headers = ClientHeaders.to_h.merge(
122
123
  "Authorization" => "Bearer #{@api_key}",
123
124
  "Flare-Project" => @project,
124
125
  "Flare-Environment" => @environment
125
- }
126
+ )
126
127
  headers["If-None-Match"] = @etag if @etag
127
128
  headers
128
129
  end