activerabbit-ai 0.6.2 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c71eb72533f7d6588444722f3ca8502230006f75bd12cee11b7f585e0a00aa1a
4
- data.tar.gz: e187bb60976eda2eccd743589737240f40930897f6063c9ff6b061e517786135
3
+ metadata.gz: 8f857d4340fe645c130c5b892b533cd4fcb7daec16f4616402dfff2b41e8fe81
4
+ data.tar.gz: 864bb6b6bacbec9e8c3777400f340977df074af989c4b7130f773876c3b8d32a
5
5
  SHA512:
6
- metadata.gz: 97ffc354514d50a73b8f32dd086e6d2a095bea00e0ce095464c7ea0a389fc8b1304359125a798abfabf4f456095aec51f3a086a2f6cb1d4c0d3eb5367d1744f0
7
- data.tar.gz: dafed4a9c5c2de391bdc1527de3092340a093afdde7cc336a04c533c7d4898d5fad4987992fa3861dbbf806dd792eaa8e2a1037cc0a00133e235cd86747a4e3e
6
+ metadata.gz: afcfc21570337a0b77e70ef7da3b7f78465c0d5eb9ba9e4cefd6491728065754385dd742b93f7c97d465fc571171dc27d5e448b547639030770b268879b4f59f
7
+ data.tar.gz: 2687df8f73f2e4dec19ce5e55af23f92c6b66649392d341dcc9309bc8acb23300180e56fa7594286d2abed5e8ec3024a8f1016accff91665e09627691cc190e7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.6.4] - 2026-04-01
6
+
7
+ ### Added
8
+ - **Log forwarding**: New `LogForwarder` buffers Rails log lines and sends them in batches to `POST /api/v1/logs`. Enable with `config.enable_logs = true` in the ActiveRabbit initializer.
9
+ - **Configuration**: `logs_flush_interval`, `logs_batch_size`, and optional `logs_source` for tuning batching and labeling forwarded logs.
10
+ - **Rails integration**: Railtie registers the forwarder with `ActiveSupport::BroadcastLogger` so `Rails.logger` output is forwarded without changing application code. Flush/shutdown hooks keep the forwarder aligned with the app lifecycle.
11
+
12
+ ## [0.6.3] - 2026-03-30
13
+
14
+ ### Added
15
+ - **HTTP client request id**: `make_request` sends a stable `X-Request-Id` header (same value across retries) so APIs can correlate or deduplicate requests after timeouts and transient failures.
16
+ - **Explicit `securerandom` load**: `HttpClient` requires `securerandom` so `SecureRandom` is always available regardless of load order.
17
+
5
18
  ## [0.6.2] - 2026-01-26
6
19
 
7
20
  ### Fixed
@@ -11,6 +11,7 @@ module ActiveRabbit
11
11
  attr_accessor :batch_size, :flush_interval, :queue_size
12
12
  attr_accessor :enable_performance_monitoring, :enable_n_plus_one_detection
13
13
  attr_accessor :enable_pii_scrubbing, :pii_fields
14
+ attr_accessor :enable_logs, :logs_flush_interval, :logs_batch_size, :logs_source
14
15
  attr_accessor :ignored_exceptions, :ignored_user_agents, :ignore_404
15
16
  attr_accessor :release, :server_name, :logger
16
17
  attr_accessor :before_send_event, :before_send_exception
@@ -40,8 +41,14 @@ module ActiveRabbit
40
41
  @enable_performance_monitoring = true
41
42
  @enable_n_plus_one_detection = true
42
43
  @enable_pii_scrubbing = true
44
+ @enable_logs = false
43
45
  @disable_console_logs = true
44
46
 
47
+ # Log forwarding
48
+ @logs_flush_interval = 5
49
+ @logs_batch_size = 100
50
+ @logs_source = nil
51
+
45
52
  # PII scrubbing
46
53
  @pii_fields = %w[
47
54
  password password_confirmation token secret key
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRabbit
4
+ module Client
5
+ # Sentry-style cron check-ins for Active Job: in_progress at start, ok on success, error on failure.
6
+ #
7
+ # class BackupJob < ApplicationJob
8
+ # include ActiveRabbit::Client::CronMonitor
9
+ # active_rabbit_cron slug: "nightly_backup"
10
+ #
11
+ # def perform
12
+ # # ...
13
+ # end
14
+ # end
15
+ module CronMonitor
16
+ def self.included(base)
17
+ base.extend(ClassMethods)
18
+ base.class_eval do
19
+ around_perform :_active_rabbit_cron_monitor_around
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ # @param value [String, Symbol, Hash] string/symbol slug, or { slug: "..." }
25
+ def active_rabbit_cron(value = nil)
26
+ case value
27
+ when String, Symbol
28
+ @active_rabbit_cron_slug = value.to_s.strip
29
+ when Hash
30
+ s = value[:slug].to_s.strip
31
+ @active_rabbit_cron_slug = s.empty? ? nil : s
32
+ when nil
33
+ @active_rabbit_cron_slug = nil
34
+ end
35
+ end
36
+
37
+ def active_rabbit_cron_monitor_slug
38
+ slug = instance_variable_defined?(:@active_rabbit_cron_slug) ? @active_rabbit_cron_slug : nil
39
+ return slug if slug.is_a?(String) && !slug.strip.empty?
40
+
41
+ name.to_s.underscore.tr("/", "_")
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def _active_rabbit_cron_monitor_around(&block)
48
+ slug = self.class.active_rabbit_cron_monitor_slug
49
+ if slug.to_s.strip.empty? || !ActiveRabbit::Client.configured?
50
+ block.call
51
+ return
52
+ end
53
+
54
+ ActiveRabbit::Client.capture_cron_check_in(slug, :in_progress)
55
+ begin
56
+ block.call
57
+ rescue StandardError => e
58
+ ActiveRabbit::Client.capture_cron_check_in(slug, :error)
59
+ raise e
60
+ end
61
+ ActiveRabbit::Client.capture_cron_check_in(slug, :ok)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -131,6 +131,9 @@ module ActiveRabbit
131
131
  structured_stack_trace: structured_frames,
132
132
  culprit_frame: culprit_frame,
133
133
 
134
+ # Error source: backend (Ruby gem) vs frontend (JS SDK)
135
+ source: "backend",
136
+
134
137
  # Timing and environment
135
138
  occurred_at: Time.now.iso8601(3),
136
139
  environment: configuration.environment || 'development',
@@ -6,6 +6,7 @@ require "json"
6
6
  require "concurrent"
7
7
  require "time"
8
8
  require "uri"
9
+ require "securerandom"
9
10
 
10
11
  module ActiveRabbit
11
12
  module Client
@@ -140,6 +141,12 @@ module ActiveRabbit
140
141
  { success: false, error: e.message }
141
142
  end
142
143
 
144
+ # Cron / heartbeat check-in (project token + monitor slug). Synchronous, not batched.
145
+ def post_cron_check_in(slug:, status: :ok)
146
+ payload = stringify_and_sanitize({ slug: slug.to_s, status: status.to_s })
147
+ make_request(:post, "/api/v1/cron/check_ins", payload)
148
+ end
149
+
143
150
  def flush
144
151
  return if @request_queue.empty?
145
152
 
@@ -225,12 +232,16 @@ module ActiveRabbit
225
232
  log(:debug, "[ActiveRabbit] Request headers: X-Project-Token=#{configuration.api_key}, X-Project-ID=#{configuration.project_id}")
226
233
  log(:debug, "[ActiveRabbit] Request body: #{safe_preview(data)}")
227
234
 
235
+ # Generate a stable request ID for dedup — same ID is reused across retries
236
+ # so the server can reject duplicates when we retry after a timeout.
237
+ request_id = SecureRandom.uuid
238
+
228
239
  # Retry logic with exponential backoff
229
240
  retries = 0
230
241
  max_retries = configuration.retry_count
231
242
 
232
243
  begin
233
- response = perform_request(uri, method, data)
244
+ response = perform_request(uri, method, data, request_id: request_id)
234
245
  log(:info, "[ActiveRabbit] Response status: #{response.code}")
235
246
  log(:debug, "[ActiveRabbit] Response headers: #{response.to_hash.inspect}")
236
247
  log(:debug, "[ActiveRabbit] Response body: #{response.body}")
@@ -273,7 +284,7 @@ module ActiveRabbit
273
284
  end
274
285
  end
275
286
 
276
- def perform_request(uri, method, data)
287
+ def perform_request(uri, method, data, request_id: nil)
277
288
  log(:debug, "[ActiveRabbit] Making HTTP request: #{method.upcase} #{uri}")
278
289
  http = Net::HTTP.new(uri.host, uri.port)
279
290
 
@@ -308,6 +319,7 @@ module ActiveRabbit
308
319
  request['Accept'] = 'application/json'
309
320
  request['User-Agent'] = "ActiveRabbit-Client/#{ActiveRabbit::Client::VERSION}"
310
321
  request['X-Project-Token'] = configuration.api_key
322
+ request['X-Request-Id'] = request_id if request_id
311
323
 
312
324
  if configuration.project_id
313
325
  request['X-Project-ID'] = configuration.project_id
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "logger"
6
+
7
+ module ActiveRabbit
8
+ module Client
9
+ # Logger subclass that buffers log entries and forwards them in batches to
10
+ # the ActiveRabbit POST /api/v1/logs endpoint.
11
+ #
12
+ # Plugged into ActiveSupport::BroadcastLogger automatically by the Railtie
13
+ # when `config.enable_logs = true`, so every Rails.logger call is forwarded
14
+ # without touching application code.
15
+ class LogForwarder < ::Logger
16
+ SEVERITY_MAP = { 0 => "debug", 1 => "info", 2 => "warn",
17
+ 3 => "error", 4 => "fatal", 5 => "info" }.freeze
18
+
19
+ MAX_MESSAGE_LEN = 10_000
20
+
21
+ def initialize(configuration)
22
+ @configuration = configuration
23
+ @buffer = []
24
+ @mutex = Mutex.new
25
+ @uri = URI("#{configuration.api_url.chomp('/')}/api/v1/logs")
26
+ @stopped = false
27
+
28
+ super(File::NULL, level: ::Logger::DEBUG)
29
+ self.formatter = proc { |*, msg| msg }
30
+
31
+ start_flusher
32
+ end
33
+
34
+ # -- Logger interface --------------------------------------------------
35
+
36
+ def add(severity, message = nil, progname = nil, &block)
37
+ return true if Thread.current[:_ar_log_sending]
38
+
39
+ severity ||= UNKNOWN
40
+ return true if severity < level
41
+
42
+ message = block&.call if message.nil? && block
43
+ message ||= progname
44
+ return true if message.nil?
45
+
46
+ msg = message.to_s.strip
47
+ return true if msg.empty?
48
+ msg = msg[0, MAX_MESSAGE_LEN] if msg.length > MAX_MESSAGE_LEN
49
+
50
+ entry = build_entry(severity, msg)
51
+ @mutex.synchronize { @buffer << entry }
52
+ async_flush if buffer_size >= batch_size
53
+ true
54
+ end
55
+
56
+ def flush
57
+ entries = swap_buffer
58
+ send_batch(entries) if entries&.any?
59
+ end
60
+
61
+ def stop
62
+ @stopped = true
63
+ flush
64
+ end
65
+
66
+ private
67
+
68
+ def build_entry(severity, message)
69
+ ctx = Thread.current[:active_rabbit_request_context]
70
+
71
+ {
72
+ level: SEVERITY_MAP[severity] || "info",
73
+ message: message,
74
+ source: @configuration.logs_source || "rails",
75
+ environment: @configuration.environment || (defined?(Rails) ? Rails.env.to_s : "production"),
76
+ occurred_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%6NZ"),
77
+ request_id: ctx&.dig(:request_id),
78
+ context: { pid: Process.pid }
79
+ }.compact
80
+ end
81
+
82
+ def batch_size
83
+ @configuration.logs_batch_size || 100
84
+ end
85
+
86
+ def flush_interval
87
+ @configuration.logs_flush_interval || 5
88
+ end
89
+
90
+ def buffer_size
91
+ @mutex.synchronize { @buffer.size }
92
+ end
93
+
94
+ def swap_buffer
95
+ @mutex.synchronize do
96
+ return nil if @buffer.empty?
97
+ out = @buffer.dup
98
+ @buffer.clear
99
+ out
100
+ end
101
+ end
102
+
103
+ def async_flush
104
+ Thread.new { flush }
105
+ end
106
+
107
+ def start_flusher
108
+ Thread.new do
109
+ loop do
110
+ sleep flush_interval
111
+ break if @stopped
112
+ flush
113
+ rescue StandardError
114
+ nil
115
+ end
116
+ end
117
+ end
118
+
119
+ def send_batch(entries)
120
+ return if entries.nil? || entries.empty?
121
+
122
+ Thread.current[:_ar_log_sending] = true
123
+
124
+ entries.each_slice(1000) do |chunk|
125
+ http = Net::HTTP.new(@uri.host, @uri.port)
126
+ http.use_ssl = (@uri.scheme == "https")
127
+ http.open_timeout = 5
128
+ http.read_timeout = 10
129
+
130
+ req = Net::HTTP::Post.new(@uri)
131
+ req["Content-Type"] = "application/json"
132
+ req["X-Project-Token"] = @configuration.api_key
133
+ req.body = { entries: chunk }.to_json
134
+
135
+ http.request(req)
136
+ end
137
+ rescue StandardError
138
+ nil
139
+ ensure
140
+ Thread.current[:_ar_log_sending] = false
141
+ end
142
+ end
143
+ end
144
+ end
@@ -325,6 +325,25 @@ module ActiveRabbit
325
325
  ar_log(:info, "[ActiveRabbit] Middleware configured successfully")
326
326
  end
327
327
 
328
+ initializer "active_rabbit.log_forwarding", after: :initialize_logger do |app|
329
+ app.config.after_initialize do
330
+ cfg = ActiveRabbit::Client.configuration
331
+ next unless cfg&.enable_logs
332
+ next unless ActiveRabbit::Client.configured?
333
+
334
+ forwarder = ActiveRabbit::Client.log_forwarder
335
+ next unless forwarder
336
+
337
+ if Rails.logger.is_a?(ActiveSupport::BroadcastLogger)
338
+ Rails.logger.broadcast_to(forwarder)
339
+ else
340
+ Rails.logger = ActiveSupport::BroadcastLogger.new(Rails.logger, forwarder)
341
+ end
342
+
343
+ ar_log(:info, "[ActiveRabbit] Log forwarding enabled")
344
+ end
345
+ end
346
+
328
347
  initializer "active_rabbit.error_reporter" do |app|
329
348
  # DISABLED: Rails error reporter creates duplicate events because middleware already catches all errors
330
349
  # The middleware provides better context and catches errors at the right level
@@ -1,5 +1,5 @@
1
1
  module ActiveRabbit
2
2
  module Client
3
- VERSION = "0.6.2"
3
+ VERSION = "0.6.4"
4
4
  end
5
5
  end
@@ -9,6 +9,7 @@ require_relative "client/performance_monitor"
9
9
  require_relative "client/n_plus_one_detector"
10
10
  require_relative "client/pii_scrubber"
11
11
  require_relative "client/error_reporter"
12
+ require_relative "client/log_forwarder"
12
13
 
13
14
  # Rails integration (optional)
14
15
  begin
@@ -24,6 +25,12 @@ rescue LoadError
24
25
  # Sidekiq not available, skip integration
25
26
  end
26
27
 
28
+ # Active Job cron monitors (optional)
29
+ begin
30
+ require_relative "client/cron_monitor" if defined?(ActiveJob)
31
+ rescue LoadError
32
+ end
33
+
27
34
  module ActiveRabbit
28
35
  module Client
29
36
  class Error < StandardError; end
@@ -111,6 +118,7 @@ module ActiveRabbit
111
118
  event_processor.flush
112
119
  exception_tracker.flush
113
120
  performance_monitor.flush
121
+ @log_forwarder&.flush
114
122
  end
115
123
 
116
124
  # Shutdown the client gracefully
@@ -118,10 +126,17 @@ module ActiveRabbit
118
126
  return unless configured?
119
127
 
120
128
  flush
129
+ @log_forwarder&.stop
121
130
  event_processor.shutdown
122
131
  http_client.shutdown
123
132
  end
124
133
 
134
+ # Returns the LogForwarder instance (created on first access when enabled).
135
+ def log_forwarder
136
+ return nil unless configuration&.enable_logs
137
+ @log_forwarder ||= LogForwarder.new(configuration)
138
+ end
139
+
125
140
  # Manual capture convenience for non-Rails contexts
126
141
  def capture_exception(exception, context: {}, user_id: nil, tags: {})
127
142
  track_exception(exception, context: context, user_id: user_id, tags: tags)
@@ -144,6 +159,19 @@ module ActiveRabbit
144
159
  http_client.post_release(payload)
145
160
  end
146
161
 
162
+ # Cron / heartbeat monitor (uses project API token + slug). Status: :ok, :success, :in_progress, :error.
163
+ def capture_cron_check_in(slug, status = :ok)
164
+ return unless configured?
165
+
166
+ s = slug.to_s.strip
167
+ return nil if s.empty?
168
+
169
+ http_client.post_cron_check_in(slug: s, status: status)
170
+ rescue => e
171
+ log(:error, "[ActiveRabbit] capture_cron_check_in failed: #{e.class}: #{e.message}")
172
+ nil
173
+ end
174
+
147
175
  def log(level, message)
148
176
  cfg = configuration
149
177
  return if cfg.nil? || cfg.disable_console_logs
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerabbit-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Shapalov
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-01-27 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: concurrent-ruby
@@ -90,11 +89,13 @@ files:
90
89
  - lib/active_rabbit/client/action_mailer_patch.rb
91
90
  - lib/active_rabbit/client/active_job_extensions.rb
92
91
  - lib/active_rabbit/client/configuration.rb
92
+ - lib/active_rabbit/client/cron_monitor.rb
93
93
  - lib/active_rabbit/client/dedupe.rb
94
94
  - lib/active_rabbit/client/error_reporter.rb
95
95
  - lib/active_rabbit/client/event_processor.rb
96
96
  - lib/active_rabbit/client/exception_tracker.rb
97
97
  - lib/active_rabbit/client/http_client.rb
98
+ - lib/active_rabbit/client/log_forwarder.rb
98
99
  - lib/active_rabbit/client/n_plus_one_detector.rb
99
100
  - lib/active_rabbit/client/performance_monitor.rb
100
101
  - lib/active_rabbit/client/pii_scrubber.rb
@@ -120,7 +121,6 @@ metadata:
120
121
  allowed_push_host: https://rubygems.org
121
122
  source_code_uri: https://github.com/bugrabbit/active_rabbit-client
122
123
  changelog_uri: https://github.com/bugrabbit/active_rabbit-client/blob/main/CHANGELOG.md
123
- post_install_message:
124
124
  rdoc_options: []
125
125
  require_paths:
126
126
  - lib
@@ -135,8 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  - !ruby/object:Gem::Version
136
136
  version: '0'
137
137
  requirements: []
138
- rubygems_version: 3.5.15
139
- signing_key:
138
+ rubygems_version: 4.0.7
140
139
  specification_version: 4
141
140
  summary: Ruby client for ActiveRabbit.ai application monitoring and error tracking
142
141
  test_files: []