brainzlab 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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE +26 -0
  4. data/README.md +311 -0
  5. data/lib/brainzlab/configuration.rb +215 -0
  6. data/lib/brainzlab/context.rb +91 -0
  7. data/lib/brainzlab/instrumentation/action_mailer.rb +181 -0
  8. data/lib/brainzlab/instrumentation/active_record.rb +111 -0
  9. data/lib/brainzlab/instrumentation/delayed_job.rb +236 -0
  10. data/lib/brainzlab/instrumentation/elasticsearch.rb +210 -0
  11. data/lib/brainzlab/instrumentation/faraday.rb +182 -0
  12. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  13. data/lib/brainzlab/instrumentation/graphql.rb +251 -0
  14. data/lib/brainzlab/instrumentation/httparty.rb +194 -0
  15. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  16. data/lib/brainzlab/instrumentation/net_http.rb +109 -0
  17. data/lib/brainzlab/instrumentation/redis.rb +331 -0
  18. data/lib/brainzlab/instrumentation/sidekiq.rb +264 -0
  19. data/lib/brainzlab/instrumentation.rb +132 -0
  20. data/lib/brainzlab/pulse/client.rb +132 -0
  21. data/lib/brainzlab/pulse/instrumentation.rb +364 -0
  22. data/lib/brainzlab/pulse/propagation.rb +241 -0
  23. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  24. data/lib/brainzlab/pulse/tracer.rb +111 -0
  25. data/lib/brainzlab/pulse.rb +224 -0
  26. data/lib/brainzlab/rails/log_formatter.rb +801 -0
  27. data/lib/brainzlab/rails/log_subscriber.rb +341 -0
  28. data/lib/brainzlab/rails/railtie.rb +590 -0
  29. data/lib/brainzlab/recall/buffer.rb +64 -0
  30. data/lib/brainzlab/recall/client.rb +86 -0
  31. data/lib/brainzlab/recall/logger.rb +118 -0
  32. data/lib/brainzlab/recall/provisioner.rb +113 -0
  33. data/lib/brainzlab/recall.rb +155 -0
  34. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  35. data/lib/brainzlab/reflex/client.rb +85 -0
  36. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  37. data/lib/brainzlab/reflex.rb +374 -0
  38. data/lib/brainzlab/version.rb +5 -0
  39. data/lib/brainzlab-sdk.rb +3 -0
  40. data/lib/brainzlab.rb +140 -0
  41. data/lib/generators/brainzlab/install/install_generator.rb +61 -0
  42. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  43. metadata +159 -0
@@ -0,0 +1,590 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ generators do
7
+ require "generators/brainzlab/install/install_generator"
8
+ end
9
+
10
+ initializer "brainzlab.configure_rails_initialization" do |app|
11
+ # Set defaults from Rails
12
+ BrainzLab.configure do |config|
13
+ config.environment ||= ::Rails.env.to_s
14
+ config.service ||= begin
15
+ ::Rails.application.class.module_parent_name.underscore
16
+ rescue StandardError
17
+ nil
18
+ end
19
+ end
20
+
21
+ # Add request context middleware (runs early)
22
+ app.middleware.insert_after ActionDispatch::RequestId, BrainzLab::Rails::Middleware
23
+ end
24
+
25
+ config.after_initialize do
26
+ # Set up custom log formatter
27
+ setup_log_formatter if BrainzLab.configuration.log_formatter_enabled
28
+
29
+ # Install instrumentation (HTTP tracking, etc.)
30
+ BrainzLab::Instrumentation.install!
31
+
32
+ # Install Pulse APM instrumentation (DB, views, cache)
33
+ BrainzLab::Pulse::Instrumentation.install!
34
+
35
+ # Hook into Rails 7+ error reporting
36
+ if defined?(::Rails.error) && ::Rails.error.respond_to?(:subscribe)
37
+ ::Rails.error.subscribe(BrainzLab::Rails::ErrorSubscriber.new)
38
+ end
39
+
40
+ # Hook into ActiveJob
41
+ if defined?(ActiveJob::Base)
42
+ ActiveJob::Base.include(BrainzLab::Rails::ActiveJobExtension)
43
+ end
44
+
45
+ # Hook into ActionController for rescue_from fallback
46
+ if defined?(ActionController::Base)
47
+ ActionController::Base.include(BrainzLab::Rails::ControllerExtension)
48
+ end
49
+
50
+ # Hook into Sidekiq if available
51
+ if defined?(Sidekiq)
52
+ Sidekiq.configure_server do |config|
53
+ config.error_handlers << BrainzLab::Rails::SidekiqErrorHandler.new
54
+ end
55
+ end
56
+ end
57
+
58
+ class << self
59
+ def setup_log_formatter
60
+ # Lazy require to ensure Rails is fully loaded
61
+ require_relative "log_formatter"
62
+ require_relative "log_subscriber"
63
+
64
+ config = BrainzLab.configuration
65
+
66
+ formatter_config = {
67
+ enabled: config.log_formatter_enabled,
68
+ colors: config.log_formatter_colors.nil? ? $stdout.tty? : config.log_formatter_colors,
69
+ hide_assets: config.log_formatter_hide_assets,
70
+ compact_assets: config.log_formatter_compact_assets,
71
+ show_params: config.log_formatter_show_params
72
+ }
73
+
74
+ # Create formatter and attach to subscriber
75
+ formatter = LogFormatter.new(formatter_config)
76
+ LogSubscriber.formatter = formatter
77
+
78
+ # Attach our subscribers
79
+ LogSubscriber.attach_to :action_controller
80
+ SqlLogSubscriber.attach_to :active_record
81
+ ViewLogSubscriber.attach_to :action_view
82
+ CableLogSubscriber.attach_to :action_cable
83
+
84
+ # Silence Rails default ActionController logging
85
+ silence_rails_logging
86
+ end
87
+
88
+ def silence_rails_logging
89
+ # Create a null logger that discards all output
90
+ null_logger = Logger.new(File::NULL)
91
+ null_logger.level = Logger::FATAL
92
+
93
+ # Silence ActiveRecord SQL logging
94
+ if defined?(ActiveRecord::Base)
95
+ ActiveRecord::Base.logger = null_logger
96
+ end
97
+
98
+ # Silence ActionController logging (the "Completed" message)
99
+ if defined?(ActionController::Base)
100
+ ActionController::Base.logger = null_logger
101
+ end
102
+
103
+ # Silence ActionView logging
104
+ if defined?(ActionView::Base)
105
+ ActionView::Base.logger = null_logger
106
+ end
107
+
108
+ # Silence the class-level loggers for specific subscribers
109
+ if defined?(ActionController::LogSubscriber)
110
+ ActionController::LogSubscriber.logger = null_logger
111
+ end
112
+
113
+ if defined?(ActionView::LogSubscriber)
114
+ ActionView::LogSubscriber.logger = null_logger
115
+ end
116
+
117
+ if defined?(ActiveRecord::LogSubscriber)
118
+ ActiveRecord::LogSubscriber.logger = null_logger
119
+ end
120
+
121
+ # Silence ActionCable logging
122
+ if defined?(ActionCable::Server::Base)
123
+ ActionCable.server.config.logger = null_logger
124
+ end
125
+
126
+ if defined?(ActionCable::Connection::TaggedLoggerProxy)
127
+ # ActionCable uses a tagged logger proxy that we need to quiet
128
+ end
129
+
130
+ # Silence the main Rails logger to remove "Started GET" messages
131
+ # Wrap the formatter to filter specific messages
132
+ if defined?(::Rails.logger) && ::Rails.logger.respond_to?(:formatter=)
133
+ original_formatter = ::Rails.logger.formatter || Logger::Formatter.new
134
+ ::Rails.logger.formatter = FilteringFormatter.new(original_formatter)
135
+ end
136
+ rescue StandardError
137
+ # Silently fail if we can't silence
138
+ end
139
+ end
140
+ end
141
+
142
+ # Filtering formatter that suppresses request-related messages
143
+ # Uses SimpleDelegator to support all formatter methods (including tagged logging)
144
+ class FilteringFormatter < SimpleDelegator
145
+ FILTERED_PATTERNS = [
146
+ /^Started (GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/,
147
+ /^Processing by/,
148
+ /^Completed \d+/,
149
+ /^Cannot render console from/,
150
+ /^Parameters:/,
151
+ /^Rendering/,
152
+ /^Rendered/,
153
+ /^\[ActionCable\] Broadcasting/,
154
+ /^\s*$/ # Empty lines
155
+ ].freeze
156
+
157
+ def call(severity, datetime, progname, msg)
158
+ return nil if should_filter?(msg)
159
+
160
+ __getobj__.call(severity, datetime, progname, msg)
161
+ end
162
+
163
+ private
164
+
165
+ def should_filter?(msg)
166
+ return false unless msg
167
+
168
+ msg_str = msg.to_s
169
+ FILTERED_PATTERNS.any? { |pattern| msg_str =~ pattern }
170
+ end
171
+ end
172
+
173
+ # Middleware for request context
174
+ class Middleware
175
+ def initialize(app)
176
+ @app = app
177
+ end
178
+
179
+ def call(env)
180
+ request = ActionDispatch::Request.new(env)
181
+ started_at = Time.now.utc
182
+
183
+ # Set request context
184
+ context = BrainzLab::Context.current
185
+ request_id = request.request_id || env["action_dispatch.request_id"]
186
+ context.request_id = request_id
187
+
188
+ # Store request_id in thread local for log subscriber
189
+ Thread.current[:brainzlab_request_id] = request_id
190
+
191
+ # Capture session_id - access session to ensure it's loaded
192
+ if request.session.respond_to?(:id)
193
+ # Force session load by accessing it
194
+ session_id = request.session.id rescue nil
195
+ context.session_id = session_id.to_s if session_id.present?
196
+ end
197
+
198
+ # Capture full request info for Reflex
199
+ context.request_method = request.request_method
200
+ context.request_path = request.path
201
+ context.request_url = request.url
202
+ context.request_params = filter_params(request.params.to_h)
203
+ context.request_headers = extract_headers(env)
204
+
205
+ # Add breadcrumb for request start
206
+ BrainzLab::Reflex.add_breadcrumb(
207
+ "#{request.request_method} #{request.path}",
208
+ category: "http.request",
209
+ level: :info,
210
+ data: { url: request.url }
211
+ )
212
+
213
+ # Add request data to Recall context
214
+ context.set_context(
215
+ path: request.path,
216
+ method: request.request_method,
217
+ ip: request.remote_ip,
218
+ user_agent: request.user_agent
219
+ )
220
+
221
+ # Extract distributed tracing context from incoming request headers
222
+ parent_context = BrainzLab::Pulse.extract!(env)
223
+
224
+ # Start Pulse trace if enabled and path not excluded
225
+ should_trace = should_trace_request?(request)
226
+ if should_trace
227
+ # Initialize spans array for this request
228
+ Thread.current[:brainzlab_pulse_spans] = []
229
+ Thread.current[:brainzlab_pulse_breakdown] = nil
230
+ BrainzLab::Pulse.start_trace(
231
+ "#{request.request_method} #{request.path}",
232
+ kind: "request",
233
+ parent_context: parent_context
234
+ )
235
+ end
236
+
237
+ status, headers, response = @app.call(env)
238
+
239
+ # Add breadcrumb for response
240
+ BrainzLab::Reflex.add_breadcrumb(
241
+ "Response #{status}",
242
+ category: "http.response",
243
+ level: status >= 400 ? :error : :info,
244
+ data: { status: status }
245
+ )
246
+
247
+ [status, headers, response]
248
+ rescue StandardError => e
249
+ # Record error in Pulse trace
250
+ if should_trace
251
+ BrainzLab::Pulse.finish_trace(
252
+ error: true,
253
+ error_class: e.class.name,
254
+ error_message: e.message
255
+ )
256
+ end
257
+ raise
258
+ ensure
259
+ # Finish Pulse trace for successful requests
260
+ if should_trace && !$!
261
+ record_pulse_trace(request, started_at, status)
262
+ end
263
+
264
+ Thread.current[:brainzlab_request_id] = nil
265
+ BrainzLab::Context.clear!
266
+ BrainzLab::Pulse::Propagation.clear!
267
+ end
268
+
269
+ def should_trace_request?(request)
270
+ return false unless BrainzLab.configuration.pulse_enabled
271
+
272
+ excluded = BrainzLab.configuration.pulse_excluded_paths || []
273
+ path = request.path
274
+
275
+ # Check if path matches any excluded pattern
276
+ !excluded.any? do |pattern|
277
+ if pattern.include?("*")
278
+ File.fnmatch?(pattern, path)
279
+ else
280
+ path.start_with?(pattern)
281
+ end
282
+ end
283
+ end
284
+
285
+ def record_pulse_trace(request, started_at, status)
286
+ ended_at = Time.now.utc
287
+ context = BrainzLab::Context.current
288
+
289
+ # Collect spans from instrumentation
290
+ spans = Thread.current[:brainzlab_pulse_spans] || []
291
+ breakdown = Thread.current[:brainzlab_pulse_breakdown] || {}
292
+
293
+
294
+ # Format spans for API
295
+ formatted_spans = spans.map do |span|
296
+ {
297
+ span_id: span[:span_id],
298
+ name: span[:name],
299
+ kind: span[:kind],
300
+ started_at: format_timestamp(span[:started_at]),
301
+ ended_at: format_timestamp(span[:ended_at]),
302
+ duration_ms: span[:duration_ms],
303
+ data: span[:data]
304
+ }
305
+ end
306
+
307
+ BrainzLab::Pulse.record_trace(
308
+ "#{request.request_method} #{request.path}",
309
+ kind: "request",
310
+ started_at: started_at,
311
+ ended_at: ended_at,
312
+ request_id: context.request_id,
313
+ request_method: request.request_method,
314
+ request_path: request.path,
315
+ controller: context.controller,
316
+ action: context.action,
317
+ status: status,
318
+ error: status.to_i >= 500,
319
+ view_ms: breakdown[:view_ms],
320
+ db_ms: breakdown[:db_ms],
321
+ spans: formatted_spans
322
+ )
323
+ rescue StandardError => e
324
+ BrainzLab.configuration.logger&.error("[BrainzLab::Pulse] Failed to record trace: #{e.message}")
325
+ ensure
326
+ # Clean up thread locals
327
+ Thread.current[:brainzlab_pulse_spans] = nil
328
+ Thread.current[:brainzlab_pulse_breakdown] = nil
329
+ end
330
+
331
+ private
332
+
333
+ def filter_params(params)
334
+ filtered = params.dup
335
+ BrainzLab::Reflex::FILTERED_PARAMS.each do |key|
336
+ filtered.delete(key)
337
+ filtered.delete(key.to_sym)
338
+ end
339
+ # Also filter nested password fields
340
+ deep_filter(filtered)
341
+ end
342
+
343
+ def deep_filter(obj)
344
+ case obj
345
+ when Hash
346
+ obj.each_with_object({}) do |(k, v), h|
347
+ if BrainzLab::Reflex::FILTERED_PARAMS.include?(k.to_s)
348
+ h[k] = "[FILTERED]"
349
+ else
350
+ h[k] = deep_filter(v)
351
+ end
352
+ end
353
+ when Array
354
+ obj.map { |v| deep_filter(v) }
355
+ else
356
+ obj
357
+ end
358
+ end
359
+
360
+ def format_timestamp(ts)
361
+ return nil unless ts
362
+
363
+ case ts
364
+ when Time, DateTime
365
+ ts.utc.iso8601(3)
366
+ when Float, Integer
367
+ Time.at(ts).utc.iso8601(3)
368
+ when String
369
+ ts
370
+ else
371
+ ts.to_s
372
+ end
373
+ end
374
+
375
+ def extract_headers(env)
376
+ headers = {}
377
+ env.each do |key, value|
378
+ next unless key.start_with?("HTTP_")
379
+ next if key == "HTTP_COOKIE"
380
+ next if key == "HTTP_AUTHORIZATION"
381
+
382
+ header_name = key.sub("HTTP_", "").split("_").map(&:capitalize).join("-")
383
+ headers[header_name] = value
384
+ end
385
+ headers
386
+ end
387
+ end
388
+
389
+ # Rails 7+ ErrorReporter subscriber
390
+ class ErrorSubscriber
391
+ def report(error, handled:, severity:, context: {}, source: nil)
392
+ # Capture both handled and unhandled, but mark them
393
+ BrainzLab::Reflex.capture(error,
394
+ handled: handled,
395
+ severity: severity.to_s,
396
+ source: source,
397
+ extra: context
398
+ )
399
+ rescue StandardError => e
400
+ BrainzLab.configuration.logger&.error("[BrainzLab] ErrorSubscriber failed: #{e.message}")
401
+ end
402
+ end
403
+
404
+ # ActionController extension for error capture
405
+ module ControllerExtension
406
+ extend ActiveSupport::Concern
407
+
408
+ included do
409
+ around_action :brainzlab_capture_context
410
+ rescue_from Exception, with: :brainzlab_capture_exception
411
+ end
412
+
413
+ private
414
+
415
+ def brainzlab_capture_context
416
+ # Set controller/action context
417
+ context = BrainzLab::Context.current
418
+ context.controller = self.class.name
419
+ context.action = action_name
420
+
421
+ # Add breadcrumb
422
+ BrainzLab::Reflex.add_breadcrumb(
423
+ "#{self.class.name}##{action_name}",
424
+ category: "controller",
425
+ level: :info
426
+ )
427
+
428
+ yield
429
+ end
430
+
431
+ def brainzlab_capture_exception(exception)
432
+ BrainzLab::Reflex.capture(exception)
433
+ raise exception # Re-raise to let Rails handle it
434
+ end
435
+ end
436
+
437
+ # ActiveJob extension for background job error capture and Pulse tracing
438
+ module ActiveJobExtension
439
+ extend ActiveSupport::Concern
440
+
441
+ included do
442
+ around_perform :brainzlab_around_perform
443
+ rescue_from Exception, with: :brainzlab_rescue_job
444
+ end
445
+
446
+ private
447
+
448
+ def brainzlab_around_perform
449
+ started_at = Time.now.utc
450
+
451
+ # Set context for Reflex and Recall
452
+ BrainzLab::Context.current.set_context(
453
+ job_class: self.class.name,
454
+ job_id: job_id,
455
+ queue_name: queue_name,
456
+ arguments: arguments.map(&:to_s).first(5) # Limit for safety
457
+ )
458
+
459
+ BrainzLab::Reflex.add_breadcrumb(
460
+ "Job #{self.class.name}",
461
+ category: "job",
462
+ level: :info,
463
+ data: { job_id: job_id, queue: queue_name }
464
+ )
465
+
466
+ # Start Pulse trace for job if enabled
467
+ should_trace = BrainzLab.configuration.pulse_enabled
468
+ if should_trace
469
+ Thread.current[:brainzlab_pulse_spans] = []
470
+ Thread.current[:brainzlab_pulse_breakdown] = nil
471
+ BrainzLab::Pulse.start_trace(self.class.name, kind: "job")
472
+ end
473
+
474
+ error_occurred = nil
475
+ begin
476
+ yield
477
+ rescue StandardError => e
478
+ error_occurred = e
479
+ raise
480
+ end
481
+ ensure
482
+ # Record Pulse trace for job
483
+ if should_trace
484
+ record_pulse_job_trace(started_at, error_occurred)
485
+ end
486
+
487
+ BrainzLab::Context.clear!
488
+ end
489
+
490
+ def record_pulse_job_trace(started_at, error = nil)
491
+ ended_at = Time.now.utc
492
+
493
+ # Collect spans from instrumentation
494
+ spans = Thread.current[:brainzlab_pulse_spans] || []
495
+ breakdown = Thread.current[:brainzlab_pulse_breakdown] || {}
496
+
497
+ # Format spans for API
498
+ formatted_spans = spans.map do |span|
499
+ {
500
+ span_id: span[:span_id],
501
+ name: span[:name],
502
+ kind: span[:kind],
503
+ started_at: format_job_timestamp(span[:started_at]),
504
+ ended_at: format_job_timestamp(span[:ended_at]),
505
+ duration_ms: span[:duration_ms],
506
+ data: span[:data]
507
+ }
508
+ end
509
+
510
+ # Calculate queue wait time if available
511
+ queue_wait_ms = nil
512
+ if respond_to?(:scheduled_at) && scheduled_at
513
+ queue_wait_ms = ((started_at - scheduled_at) * 1000).round(2)
514
+ elsif respond_to?(:enqueued_at) && enqueued_at
515
+ queue_wait_ms = ((started_at - enqueued_at) * 1000).round(2)
516
+ end
517
+
518
+ BrainzLab::Pulse.record_trace(
519
+ self.class.name,
520
+ kind: "job",
521
+ started_at: started_at,
522
+ ended_at: ended_at,
523
+ job_class: self.class.name,
524
+ job_id: job_id,
525
+ queue: queue_name,
526
+ error: error.present?,
527
+ error_class: error&.class&.name,
528
+ error_message: error&.message,
529
+ db_ms: breakdown[:db_ms],
530
+ queue_wait_ms: queue_wait_ms,
531
+ executions: executions,
532
+ spans: formatted_spans
533
+ )
534
+ rescue StandardError => e
535
+ BrainzLab.configuration.logger&.error("[BrainzLab::Pulse] Failed to record job trace: #{e.message}")
536
+ ensure
537
+ # Clean up thread locals
538
+ Thread.current[:brainzlab_pulse_spans] = nil
539
+ Thread.current[:brainzlab_pulse_breakdown] = nil
540
+ end
541
+
542
+ def format_job_timestamp(ts)
543
+ return nil unless ts
544
+
545
+ case ts
546
+ when Time, DateTime
547
+ ts.utc.iso8601(3)
548
+ when Float, Integer
549
+ Time.at(ts).utc.iso8601(3)
550
+ when String
551
+ ts
552
+ else
553
+ ts.to_s
554
+ end
555
+ end
556
+
557
+ def brainzlab_rescue_job(exception)
558
+ BrainzLab::Reflex.capture(exception,
559
+ tags: { type: "background_job" },
560
+ extra: {
561
+ job_class: self.class.name,
562
+ job_id: job_id,
563
+ queue_name: queue_name,
564
+ executions: executions,
565
+ arguments: arguments.map(&:to_s).first(5)
566
+ }
567
+ )
568
+ raise exception # Re-raise to let ActiveJob handle retries
569
+ end
570
+ end
571
+
572
+ # Sidekiq error handler
573
+ class SidekiqErrorHandler
574
+ def call(exception, context)
575
+ BrainzLab::Reflex.capture(exception,
576
+ tags: { type: "sidekiq" },
577
+ extra: {
578
+ job_class: context[:job]["class"],
579
+ job_id: context[:job]["jid"],
580
+ queue: context[:job]["queue"],
581
+ args: context[:job]["args"]&.map(&:to_s)&.first(5),
582
+ retry_count: context[:job]["retry_count"]
583
+ }
584
+ )
585
+ rescue StandardError => e
586
+ BrainzLab.configuration.logger&.error("[BrainzLab] Sidekiq handler failed: #{e.message}")
587
+ end
588
+ end
589
+ end
590
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module BrainzLab
6
+ module Recall
7
+ class Buffer
8
+ def initialize(config, client)
9
+ @config = config
10
+ @client = client
11
+ @buffer = Concurrent::Array.new
12
+ @mutex = Mutex.new
13
+ @flush_thread = nil
14
+ @shutdown = false
15
+
16
+ start_flush_thread
17
+ setup_at_exit
18
+ end
19
+
20
+ def push(log_entry)
21
+ @buffer.push(log_entry)
22
+ flush if @buffer.size >= @config.recall_buffer_size
23
+ end
24
+
25
+ def flush
26
+ return if @buffer.empty?
27
+
28
+ entries = nil
29
+ @mutex.synchronize do
30
+ entries = @buffer.dup
31
+ @buffer.clear
32
+ end
33
+
34
+ return if entries.nil? || entries.empty?
35
+
36
+ @client.send_batch(entries)
37
+ end
38
+
39
+ def shutdown
40
+ @shutdown = true
41
+ @flush_thread&.kill
42
+ flush
43
+ end
44
+
45
+ private
46
+
47
+ def start_flush_thread
48
+ @flush_thread = Thread.new do
49
+ loop do
50
+ break if @shutdown
51
+
52
+ sleep(@config.recall_flush_interval)
53
+ flush unless @shutdown
54
+ end
55
+ end
56
+ @flush_thread.abort_on_exception = false
57
+ end
58
+
59
+ def setup_at_exit
60
+ at_exit { shutdown }
61
+ end
62
+ end
63
+ end
64
+ end