rails_event_viewer 0.1.2 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +11 -20
- data/app/views/layouts/rails_event_viewer/application.html.erb +8 -0
- data/lib/generators/rails_event_viewer/install/templates/initializer.rb.erb +5 -0
- data/lib/rails_event_viewer/batch_writer.rb +79 -0
- data/lib/rails_event_viewer/buffer.rb +28 -0
- data/lib/rails_event_viewer/buffers/memory.rb +36 -0
- data/lib/rails_event_viewer/flusher.rb +150 -0
- data/lib/rails_event_viewer/logging.rb +19 -0
- data/lib/rails_event_viewer/subscriber.rb +9 -148
- data/lib/rails_event_viewer/version.rb +1 -1
- data/lib/rails_event_viewer.rb +6 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26f6b1798d95bf71759a16da1fd092f8d8296a4118378a8c5ce84e12848efe28
|
|
4
|
+
data.tar.gz: b290b0d5803b71aa2f8fda06ee160870672a4073c175c2bb6a0e0856f19b2dbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef7fc01c18016ff1f9d48de98fcc1f42b248ac1fea588806c6dc3a409b12f75291e4165ee59ff45220b93a024f6b816d7cb2dba4395c2b5c441dd6f323a8a17a
|
|
7
|
+
data.tar.gz: 896dd1e41a71c0c0bd25dbdd0a376ffd7c5c0c68b8d7d6cf445521127560d95ba0d351e1d9f9f4c6c5d73ed00ae6472dbd9096ceddf2d79cd8b1dbf4e3edb37f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-10-01)
|
|
4
|
+
|
|
5
|
+
- Pluggable buffers: `config.buffer` accepts any object implementing `RailsEventViewer::Buffer` (`push`, `drain`, `commit`, `revert`, `dead`, `size`, `after_fork`). `push` returns the new size. The in-memory buffer remains the default.
|
|
6
|
+
- A failed batch is reverted and retried with exponential backoff. After three failures it is written one event at a time.
|
|
7
|
+
- Events the adapter keeps rejecting, and events dropped by the buffer cap, are passed to `dead` instead of being lost silently.
|
|
8
|
+
- A failed sync write, or a failed `push` to the buffer in async mode, is now logged and re-raised to `Rails.event`, which reports it to `Rails.error` (or raises, with `raise_on_error`), instead of being swallowed.
|
|
9
|
+
- A banner warns when `sample_rate` is below 1.0, since timelines can have gaps.
|
|
10
|
+
- README now marks the gem as experimental and not yet recommended for production.
|
|
11
|
+
|
|
12
|
+
## 0.1.3 (2026-10-01)
|
|
13
|
+
|
|
14
|
+
- The flusher thread starts on the first buffered event, not at boot, so consoles and rake tasks no longer run an idle thread.
|
|
15
|
+
- Flusher start/stop messages log at debug level.
|
|
16
|
+
- Events emitted after shutdown are written immediately instead of lost.
|
|
17
|
+
- Web requests never write to storage. A full buffer wakes the flusher thread instead.
|
|
18
|
+
- The buffer is capped at 10 times `buffer_size`. Overflow drops the oldest events and logs a warning.
|
|
19
|
+
- Removed the unused ActionCable broadcast.
|
|
20
|
+
|
|
3
21
|
## 0.1.2 (2026-09-30)
|
|
4
22
|
|
|
5
23
|
- Removed the `pagy` dependency. Pagination is handled by a small built-in class, so host apps on any Pagy version, or none, can install the gem.
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A Rails engine that captures events emitted with `Rails.event` and gives you a dashboard to browse, search, and chart them.
|
|
4
4
|
|
|
5
|
+
> **Experimental.** This gem is in early development and not recommended for production yet. APIs, the database schema, and behavior may change between minor versions.
|
|
6
|
+
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
7
9
|
- Rails 8.1+
|
|
@@ -61,8 +63,15 @@ RailsEventViewer.configure do |config|
|
|
|
61
63
|
end
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
With `async`, events are written by a background thread, never by your web requests. The buffer holds at most 10 times `buffer_size` events. If storage can't keep up, the oldest are dropped and a warning is logged. Events still in the buffer are written on a normal shutdown but lost if the process is killed.
|
|
67
|
+
|
|
64
68
|
Internal Rails events such as `active_record.*` and `action_controller.*` are ignored by default. Remove a pattern from `ignored_events` to capture it.
|
|
65
69
|
|
|
70
|
+
## Sync vs async
|
|
71
|
+
|
|
72
|
+
- With `async = false`, each event is written inside your transaction, and a failed write is reported to `Rails.error`. In development and test, where Rails sets `raise_on_error`, it raises instead.
|
|
73
|
+
- With `async = true`, events wait in an in-memory buffer and are lost if the process is killed. Set `config.buffer` to your own `RailsEventViewer::Buffer`, for example backed by Redis, to keep them.
|
|
74
|
+
|
|
66
75
|
## Authentication
|
|
67
76
|
|
|
68
77
|
Without authentication, the dashboard is open in development and test, and returns `403` everywhere else.
|
|
@@ -83,27 +92,9 @@ Set `RAILS_ENV` on every server. Without it, Rails falls back to development and
|
|
|
83
92
|
|
|
84
93
|
## Storage adapters
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|---|---|
|
|
88
|
-
| `:active_record` | The default. Stores events in your database, with full analytics. |
|
|
89
|
-
| `:redis` | High volume, short-lived storage. Keeps the newest `max_events`. |
|
|
90
|
-
| `:memory` | Development and tests. Lost on restart. |
|
|
91
|
-
| `:null` | Disables storage. |
|
|
92
|
-
|
|
93
|
-
Redis needs the `redis` gem:
|
|
94
|
-
|
|
95
|
-
```ruby
|
|
96
|
-
config.storage_adapter = :redis
|
|
97
|
-
config.adapter_options = {
|
|
98
|
-
redis_options: { url: ENV["REDIS_URL"] },
|
|
99
|
-
pool_size: 5,
|
|
100
|
-
max_events: 10_000
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Filtered queries on Redis scan every stored event, so keep `max_events` modest.
|
|
95
|
+
Set `config.storage_adapter` to `:active_record` (default), `:redis`, `:memory`, or `:null`. Redis needs the `redis` gem and takes options through `config.adapter_options`.
|
|
105
96
|
|
|
106
|
-
|
|
97
|
+
Filtered queries on Redis scan every stored event, so keep `max_events` (how many events Redis keeps, set in `adapter_options`) modest. For your own storage, include `RailsEventViewer::Adapter`.
|
|
107
98
|
|
|
108
99
|
## Rake tasks
|
|
109
100
|
|
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
<% end %>
|
|
35
35
|
|
|
36
|
+
<% if RailsEventViewer.sample_rate.to_f < 1.0 %>
|
|
37
|
+
<div class="mb-4 rounded-md bg-yellow-50 border border-yellow-200 p-4">
|
|
38
|
+
<p class="text-sm font-medium text-yellow-800">
|
|
39
|
+
Sampling is on: only <%= number_to_percentage(RailsEventViewer.sample_rate.to_f * 100, precision: 3, significant: true, strip_insignificant_zeros: true) %> of events are recorded, so timelines can have gaps.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
<% end %>
|
|
43
|
+
|
|
36
44
|
<%= yield %>
|
|
37
45
|
</div>
|
|
38
46
|
</main>
|
|
@@ -40,6 +40,11 @@ RailsEventViewer.configure do |config|
|
|
|
40
40
|
# Flush interval in seconds (flushes even if buffer isn't full)
|
|
41
41
|
# config.flush_interval = 2
|
|
42
42
|
|
|
43
|
+
# Buffer used when async is true. The default is in memory, so buffered events
|
|
44
|
+
# are lost if the process is killed. You can write your own buffers (see RailsEventViewer::Buffer)
|
|
45
|
+
# to persist them, for example in Redis.
|
|
46
|
+
# config.buffer = MyRedisBuffer.new
|
|
47
|
+
|
|
43
48
|
# Sample rate: 1.0 = capture all events, 0.1 = capture 10% of events
|
|
44
49
|
# Useful for high-volume production environments
|
|
45
50
|
# config.sample_rate = 1.0
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module RailsEventViewer
|
|
2
|
+
class BatchWriter
|
|
3
|
+
include Logging
|
|
4
|
+
|
|
5
|
+
MAX_ATTEMPTS = 3
|
|
6
|
+
LOGGED_EVENTS = 10
|
|
7
|
+
|
|
8
|
+
attr_reader :failed_attempts
|
|
9
|
+
|
|
10
|
+
def initialize(buffer)
|
|
11
|
+
@buffer = buffer
|
|
12
|
+
reset
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reset
|
|
16
|
+
@failed_attempts = 0
|
|
17
|
+
@strikes = Hash.new(0)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def write(events)
|
|
21
|
+
RailsEventViewer.adapter.write_events(events)
|
|
22
|
+
@buffer.commit(events)
|
|
23
|
+
forget(events)
|
|
24
|
+
@failed_attempts = 0
|
|
25
|
+
true
|
|
26
|
+
rescue => e
|
|
27
|
+
log_error("[RailsEventViewer] Failed to write #{events.size} events: #{e.message}")
|
|
28
|
+
@failed_attempts += 1
|
|
29
|
+
return write_individually(events) if @failed_attempts >= MAX_ATTEMPTS
|
|
30
|
+
|
|
31
|
+
@buffer.revert(events)
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def forget(events)
|
|
36
|
+
events.each { |event| @strikes.delete(event) } unless @strikes.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def write_individually(events)
|
|
42
|
+
written = []
|
|
43
|
+
rejected = []
|
|
44
|
+
events.each do |event|
|
|
45
|
+
(write_one(event) ? written : rejected) << event
|
|
46
|
+
break if written.empty? && rejected.size >= MAX_ATTEMPTS
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if written.empty?
|
|
50
|
+
@buffer.revert(events)
|
|
51
|
+
return false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
rejected.each { |event| @strikes[event] += 1 }
|
|
55
|
+
dead, retry_later = rejected.partition { |event| @strikes[event] >= MAX_ATTEMPTS }
|
|
56
|
+
forget(written + dead)
|
|
57
|
+
|
|
58
|
+
@buffer.commit(written)
|
|
59
|
+
@buffer.revert(retry_later) if retry_later.any?
|
|
60
|
+
give_up(dead) if dead.any?
|
|
61
|
+
@failed_attempts = retry_later.any? ? MAX_ATTEMPTS - 1 : 0
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def give_up(events)
|
|
66
|
+
names = events.first(LOGGED_EVENTS).map { |event| event[:name] }.join(", ")
|
|
67
|
+
log_error("[RailsEventViewer] Gave up on #{events.size} events the adapter rejected #{MAX_ATTEMPTS} times (#{@last_rejection}): #{names}")
|
|
68
|
+
@buffer.dead(events)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_one(event)
|
|
72
|
+
RailsEventViewer.adapter.write_events([event])
|
|
73
|
+
true
|
|
74
|
+
rescue => e
|
|
75
|
+
@last_rejection = e.message
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module RailsEventViewer
|
|
2
|
+
module Buffer
|
|
3
|
+
def push(entry)
|
|
4
|
+
raise NotImplementedError, "#{self.class} must implement #push"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def drain(limit)
|
|
8
|
+
raise NotImplementedError, "#{self.class} must implement #drain"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def commit(entries)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def revert(entries)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dead(entries)
|
|
18
|
+
commit(entries)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def size
|
|
22
|
+
raise NotImplementedError, "#{self.class} must implement #size"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def after_fork
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module RailsEventViewer
|
|
2
|
+
module Buffers
|
|
3
|
+
class Memory
|
|
4
|
+
include Buffer
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@queue = Queue.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def push(entry)
|
|
11
|
+
@queue << entry
|
|
12
|
+
@queue.size
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def drain(limit)
|
|
16
|
+
entries = []
|
|
17
|
+
limit.times { entries << @queue.pop(true) }
|
|
18
|
+
entries
|
|
19
|
+
rescue ThreadError
|
|
20
|
+
entries
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def revert(entries)
|
|
24
|
+
entries.each { |entry| @queue << entry }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def size
|
|
28
|
+
@queue.size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def after_fork
|
|
32
|
+
@queue = Queue.new
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
module RailsEventViewer
|
|
2
|
+
class Flusher
|
|
3
|
+
include Logging
|
|
4
|
+
|
|
5
|
+
SHUTDOWN_TIMEOUT_SECONDS = 5
|
|
6
|
+
MAX_BUFFER_MULTIPLIER = 10
|
|
7
|
+
|
|
8
|
+
def initialize(buffer)
|
|
9
|
+
@buffer = buffer
|
|
10
|
+
@writer = BatchWriter.new(buffer)
|
|
11
|
+
@flush_requests = Queue.new
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@thread = nil
|
|
14
|
+
@shutdown_requested = false
|
|
15
|
+
@dropped_count = 0
|
|
16
|
+
@pid = Process.pid
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def push(entry)
|
|
20
|
+
handle_fork_if_needed
|
|
21
|
+
|
|
22
|
+
size = @buffer.push(entry)
|
|
23
|
+
return flush! if shutdown_requested?
|
|
24
|
+
|
|
25
|
+
ensure_thread!
|
|
26
|
+
drop_oldest_events if size > max_buffer_size
|
|
27
|
+
request_flush if size >= RailsEventViewer.buffer_size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def flush!
|
|
31
|
+
events = @buffer.drain([@buffer.size, max_buffer_size].min)
|
|
32
|
+
return true if events.empty?
|
|
33
|
+
|
|
34
|
+
written = @writer.write(events)
|
|
35
|
+
report_dropped_events if written
|
|
36
|
+
written
|
|
37
|
+
rescue => e
|
|
38
|
+
log_error("[RailsEventViewer] Failed to flush: #{e.message}")
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stop!
|
|
43
|
+
@mutex.synchronize { @shutdown_requested = true }
|
|
44
|
+
|
|
45
|
+
if @thread&.alive?
|
|
46
|
+
@flush_requests << true
|
|
47
|
+
@thread.join(SHUTDOWN_TIMEOUT_SECONDS)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@mutex.synchronize { @thread = nil }
|
|
51
|
+
|
|
52
|
+
loop do
|
|
53
|
+
remaining = @buffer.size
|
|
54
|
+
break if remaining.zero?
|
|
55
|
+
|
|
56
|
+
flush!
|
|
57
|
+
break if @buffer.size >= remaining
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def running?
|
|
62
|
+
@thread&.alive? || false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def size
|
|
66
|
+
@buffer.size
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def handle_fork_if_needed
|
|
72
|
+
@mutex.synchronize do
|
|
73
|
+
if @pid != Process.pid
|
|
74
|
+
@pid = Process.pid
|
|
75
|
+
@buffer.after_fork
|
|
76
|
+
@writer.reset
|
|
77
|
+
@flush_requests = Queue.new
|
|
78
|
+
@thread = nil
|
|
79
|
+
@shutdown_requested = false
|
|
80
|
+
@dropped_count = 0
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def max_buffer_size
|
|
86
|
+
RailsEventViewer.buffer_size * MAX_BUFFER_MULTIPLIER
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def drop_oldest_events
|
|
90
|
+
while @buffer.size > max_buffer_size
|
|
91
|
+
dropped = @buffer.drain(1)
|
|
92
|
+
break if dropped.empty?
|
|
93
|
+
|
|
94
|
+
@buffer.dead(dropped)
|
|
95
|
+
@writer.forget(dropped)
|
|
96
|
+
@mutex.synchronize { @dropped_count += 1 }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def report_dropped_events
|
|
101
|
+
dropped = @mutex.synchronize { @dropped_count.tap { @dropped_count = 0 } }
|
|
102
|
+
return if dropped.zero?
|
|
103
|
+
|
|
104
|
+
effective_logger&.warn("[RailsEventViewer] Dropped #{dropped} events because the buffer was full")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def request_flush
|
|
108
|
+
@flush_requests << true if @flush_requests.empty? && @writer.failed_attempts.zero?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ensure_thread!
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
return if @thread&.alive?
|
|
114
|
+
return if @shutdown_requested
|
|
115
|
+
|
|
116
|
+
@thread = Thread.new { run_loop }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def run_loop
|
|
121
|
+
effective_logger&.debug("[RailsEventViewer] Flusher thread started (PID: #{Process.pid})")
|
|
122
|
+
|
|
123
|
+
until shutdown_requested?
|
|
124
|
+
begin
|
|
125
|
+
wait_for_flush
|
|
126
|
+
loop do
|
|
127
|
+
break unless flush!
|
|
128
|
+
break if @buffer.size < RailsEventViewer.buffer_size || shutdown_requested?
|
|
129
|
+
end
|
|
130
|
+
rescue => e
|
|
131
|
+
log_error("[RailsEventViewer] Flusher error: #{e.message}")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
effective_logger&.debug("[RailsEventViewer] Flusher thread stopped (PID: #{Process.pid})")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def wait_for_flush
|
|
139
|
+
backoff = 2**[@writer.failed_attempts, BatchWriter::MAX_ATTEMPTS].min
|
|
140
|
+
@flush_requests.clear unless @writer.failed_attempts.zero?
|
|
141
|
+
return if shutdown_requested?
|
|
142
|
+
|
|
143
|
+
@flush_requests.pop(timeout: RailsEventViewer.flush_interval * backoff)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def shutdown_requested?
|
|
147
|
+
@mutex.synchronize { @shutdown_requested }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module RailsEventViewer
|
|
2
|
+
module Logging
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def log_error(message)
|
|
6
|
+
logger = effective_logger
|
|
7
|
+
|
|
8
|
+
if logger
|
|
9
|
+
logger.error(message)
|
|
10
|
+
else
|
|
11
|
+
warn message
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def effective_logger
|
|
16
|
+
RailsEventViewer.logger || (defined?(Rails) && Rails.logger)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -2,156 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module RailsEventViewer
|
|
4
4
|
class Subscriber
|
|
5
|
+
include Logging
|
|
6
|
+
|
|
5
7
|
NANOSECONDS_PER_SECOND = 1_000_000_000.0
|
|
6
|
-
SHUTDOWN_TIMEOUT_SECONDS = 5
|
|
7
|
-
ACTION_CABLE_CHANNEL = "rails_event_viewer:events"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@mutex = Mutex.new
|
|
12
|
-
@flusher_thread = nil
|
|
13
|
-
@shutdown_requested = false
|
|
14
|
-
@pid = Process.pid
|
|
9
|
+
delegate :flush!, :stop!, :running?, to: :@flusher
|
|
10
|
+
delegate :size, to: :@flusher, prefix: :buffer
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
def initialize
|
|
13
|
+
@flusher = Flusher.new(RailsEventViewer.buffer)
|
|
17
14
|
end
|
|
18
15
|
|
|
19
16
|
def emit(event)
|
|
20
|
-
handle_fork_if_needed
|
|
21
|
-
|
|
22
17
|
return unless should_capture?(event)
|
|
23
18
|
return if sampled_out?
|
|
24
19
|
|
|
25
20
|
entry = build_entry(event)
|
|
26
21
|
|
|
27
22
|
if RailsEventViewer.async
|
|
28
|
-
|
|
23
|
+
@flusher.push(entry)
|
|
29
24
|
else
|
|
30
|
-
|
|
25
|
+
RailsEventViewer.adapter.write_events([entry])
|
|
31
26
|
end
|
|
32
27
|
rescue => e
|
|
33
28
|
handle_error(e, event)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def flush!
|
|
37
|
-
return if @buffer.empty?
|
|
38
|
-
|
|
39
|
-
events_to_write = drain_buffer
|
|
40
|
-
|
|
41
|
-
return if events_to_write.empty?
|
|
42
|
-
|
|
43
|
-
write_immediately(events_to_write)
|
|
44
|
-
broadcast_events(events_to_write)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def stop!
|
|
48
|
-
@mutex.synchronize do
|
|
49
|
-
@shutdown_requested = true
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
if @flusher_thread&.alive?
|
|
53
|
-
@flusher_thread.wakeup rescue nil
|
|
54
|
-
@flusher_thread.join(SHUTDOWN_TIMEOUT_SECONDS)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
@mutex.synchronize do
|
|
58
|
-
@flusher_thread = nil
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
flush!
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def running?
|
|
65
|
-
@flusher_thread&.alive? || false
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def buffer_size
|
|
69
|
-
@buffer.size
|
|
29
|
+
raise
|
|
70
30
|
end
|
|
71
31
|
|
|
72
32
|
private
|
|
73
33
|
|
|
74
|
-
def handle_fork_if_needed
|
|
75
|
-
# Threads don't survive forks, so we need to restart the flusher
|
|
76
|
-
@mutex.synchronize do
|
|
77
|
-
if @pid != Process.pid
|
|
78
|
-
@pid = Process.pid
|
|
79
|
-
@buffer = Queue.new
|
|
80
|
-
@flusher_thread = nil
|
|
81
|
-
@shutdown_requested = false
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
ensure_flusher_thread!
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def drain_buffer
|
|
88
|
-
events = []
|
|
89
|
-
loop { events << @buffer.pop(true) }
|
|
90
|
-
rescue ThreadError
|
|
91
|
-
events
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def buffer_event(entry)
|
|
95
|
-
@buffer << entry
|
|
96
|
-
ensure_flusher_thread!
|
|
97
|
-
|
|
98
|
-
flush! if @buffer.size >= RailsEventViewer.buffer_size
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def ensure_flusher_thread!
|
|
102
|
-
return unless RailsEventViewer.async
|
|
103
|
-
|
|
104
|
-
@mutex.synchronize do
|
|
105
|
-
return if @flusher_thread&.alive?
|
|
106
|
-
return if @shutdown_requested
|
|
107
|
-
|
|
108
|
-
@flusher_thread = Thread.new { run_flusher_loop }
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def run_flusher_loop
|
|
113
|
-
log_info("[RailsEventViewer] Flusher thread started (PID: #{Process.pid})")
|
|
114
|
-
|
|
115
|
-
until shutdown_requested?
|
|
116
|
-
begin
|
|
117
|
-
sleep(RailsEventViewer.flush_interval)
|
|
118
|
-
flush! unless @buffer.empty?
|
|
119
|
-
rescue => e
|
|
120
|
-
log_error("[RailsEventViewer] Flusher error: #{e.message}")
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
log_info("[RailsEventViewer] Flusher thread stopped (PID: #{Process.pid})")
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def shutdown_requested?
|
|
128
|
-
@mutex.synchronize { @shutdown_requested }
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def write_immediately(events)
|
|
132
|
-
return if events.blank?
|
|
133
|
-
|
|
134
|
-
RailsEventViewer.adapter.write_events(events)
|
|
135
|
-
rescue => e
|
|
136
|
-
log_error("[RailsEventViewer] Failed to write #{events.size} events: #{e.message}")
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def broadcast_events(events)
|
|
140
|
-
return unless defined?(ActionCable)
|
|
141
|
-
return unless events.any?
|
|
142
|
-
|
|
143
|
-
ActionCable.server.broadcast(
|
|
144
|
-
ACTION_CABLE_CHANNEL,
|
|
145
|
-
{
|
|
146
|
-
action: "new_events",
|
|
147
|
-
count: events.size,
|
|
148
|
-
events: events.map { |e| { name: e[:name], occurred_at: e[:occurred_at]&.iso8601 } }
|
|
149
|
-
}
|
|
150
|
-
)
|
|
151
|
-
rescue => e
|
|
152
|
-
log_error("[RailsEventViewer] Broadcast error: #{e.message}")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
34
|
def should_capture?(event)
|
|
156
35
|
return false unless event.is_a?(Hash)
|
|
157
36
|
return false if event[:name].blank?
|
|
@@ -233,23 +112,5 @@ module RailsEventViewer
|
|
|
233
112
|
def handle_error(error, event)
|
|
234
113
|
log_error("[RailsEventViewer] Failed to capture event '#{event[:name]}': #{error.message}")
|
|
235
114
|
end
|
|
236
|
-
|
|
237
|
-
def log_info(message)
|
|
238
|
-
effective_logger&.info(message)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def log_error(message)
|
|
242
|
-
logger = effective_logger
|
|
243
|
-
|
|
244
|
-
if logger
|
|
245
|
-
logger.error(message)
|
|
246
|
-
else
|
|
247
|
-
warn message
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def effective_logger
|
|
252
|
-
RailsEventViewer.logger || (defined?(Rails) && Rails.logger)
|
|
253
|
-
end
|
|
254
115
|
end
|
|
255
116
|
end
|
data/lib/rails_event_viewer.rb
CHANGED
|
@@ -3,6 +3,8 @@ require "groupdate"
|
|
|
3
3
|
|
|
4
4
|
require "rails_event_viewer/version"
|
|
5
5
|
require "rails_event_viewer/engine"
|
|
6
|
+
require "rails_event_viewer/buffer"
|
|
7
|
+
require "rails_event_viewer/buffers/memory"
|
|
6
8
|
|
|
7
9
|
module RailsEventViewer
|
|
8
10
|
mattr_accessor :storage_adapter, default: :active_record
|
|
@@ -12,6 +14,7 @@ module RailsEventViewer
|
|
|
12
14
|
mattr_accessor :buffer_size, default: 100 # Flush after N events
|
|
13
15
|
mattr_accessor :flush_interval, default: 2 # Flush every N seconds
|
|
14
16
|
mattr_accessor :sample_rate, default: 1.0 # 1.0 = 100%, 0.1 = 10%
|
|
17
|
+
mattr_accessor :buffer, default: Buffers::Memory.new
|
|
15
18
|
|
|
16
19
|
mattr_accessor :retention_period, default: 7.days
|
|
17
20
|
mattr_accessor :captured_events, default: [] # Empty = capture all
|
|
@@ -115,6 +118,9 @@ end
|
|
|
115
118
|
|
|
116
119
|
require "rails_event_viewer/adapter"
|
|
117
120
|
require "rails_event_viewer/events_relation"
|
|
121
|
+
require "rails_event_viewer/logging"
|
|
122
|
+
require "rails_event_viewer/batch_writer"
|
|
123
|
+
require "rails_event_viewer/flusher"
|
|
118
124
|
require "rails_event_viewer/subscriber"
|
|
119
125
|
require "rails_event_viewer/json_query"
|
|
120
126
|
require "rails_event_viewer/time_utils"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_event_viewer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keshav Biswa
|
|
@@ -107,9 +107,14 @@ files:
|
|
|
107
107
|
- lib/rails_event_viewer/adapters/memory.rb
|
|
108
108
|
- lib/rails_event_viewer/adapters/null.rb
|
|
109
109
|
- lib/rails_event_viewer/adapters/redis.rb
|
|
110
|
+
- lib/rails_event_viewer/batch_writer.rb
|
|
111
|
+
- lib/rails_event_viewer/buffer.rb
|
|
112
|
+
- lib/rails_event_viewer/buffers/memory.rb
|
|
110
113
|
- lib/rails_event_viewer/engine.rb
|
|
111
114
|
- lib/rails_event_viewer/events_relation.rb
|
|
115
|
+
- lib/rails_event_viewer/flusher.rb
|
|
112
116
|
- lib/rails_event_viewer/json_query.rb
|
|
117
|
+
- lib/rails_event_viewer/logging.rb
|
|
113
118
|
- lib/rails_event_viewer/subscriber.rb
|
|
114
119
|
- lib/rails_event_viewer/time_utils.rb
|
|
115
120
|
- lib/rails_event_viewer/version.rb
|