dead_bro 0.2.1 → 0.2.2
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/lib/dead_bro/cache_subscriber.rb +22 -1
- data/lib/dead_bro/client.rb +73 -0
- data/lib/dead_bro/configuration.rb +3 -1
- data/lib/dead_bro/http_instrumentation.rb +24 -4
- data/lib/dead_bro/job_queue_monitor.rb +395 -0
- data/lib/dead_bro/job_sql_tracking_middleware.rb +4 -0
- data/lib/dead_bro/job_subscriber.rb +16 -0
- data/lib/dead_bro/railtie.rb +7 -0
- data/lib/dead_bro/redis_subscriber.rb +25 -4
- data/lib/dead_bro/sql_subscriber.rb +29 -4
- data/lib/dead_bro/sql_tracking_middleware.rb +5 -1
- data/lib/dead_bro/version.rb +1 -1
- data/lib/dead_bro/view_rendering_subscriber.rb +20 -1
- data/lib/dead_bro.rb +15 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64333e774f85e0505014b6a0a6170e6721c0be30f1d3103b214fabdc7faacd8f
|
|
4
|
+
data.tar.gz: 82d6fc30651fcc2ee54904c6dff7b7cc36bd56c1a27a58ee726429c8b592b50a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7fc637b60a2990c3e3cfd6c39a5adbbebde198b11c1da5ff1127199ee05e900c6b971bccc740b1552f53156717511c2244dc7e70f736ac2b2110a2e0291d577
|
|
7
|
+
data.tar.gz: 1bed47b5f0d4dabc1e851631c00701802207769280b82d2809afd7cf75fa3e5be5dfaf5940ed853234dfdf659911512f56a383020b2bb027bce6eea8d1288637
|
|
@@ -5,6 +5,7 @@ require "active_support/notifications"
|
|
|
5
5
|
module DeadBro
|
|
6
6
|
class CacheSubscriber
|
|
7
7
|
THREAD_LOCAL_KEY = :dead_bro_cache_events
|
|
8
|
+
MAX_TRACKED_EVENTS = 1000
|
|
8
9
|
|
|
9
10
|
EVENTS = [
|
|
10
11
|
"cache_read.active_support",
|
|
@@ -24,7 +25,9 @@ module DeadBro
|
|
|
24
25
|
|
|
25
26
|
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
26
27
|
event = build_event(name, data, duration_ms)
|
|
27
|
-
|
|
28
|
+
if event && should_continue_tracking?
|
|
29
|
+
Thread.current[THREAD_LOCAL_KEY] << event
|
|
30
|
+
end
|
|
28
31
|
end
|
|
29
32
|
rescue
|
|
30
33
|
end
|
|
@@ -42,6 +45,24 @@ module DeadBro
|
|
|
42
45
|
events || []
|
|
43
46
|
end
|
|
44
47
|
|
|
48
|
+
# Check if we should continue tracking based on count and time limits
|
|
49
|
+
def self.should_continue_tracking?
|
|
50
|
+
events = Thread.current[THREAD_LOCAL_KEY]
|
|
51
|
+
return false unless events
|
|
52
|
+
|
|
53
|
+
# Check count limit
|
|
54
|
+
return false if events.length >= MAX_TRACKED_EVENTS
|
|
55
|
+
|
|
56
|
+
# Check time limit
|
|
57
|
+
start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
58
|
+
if start_time
|
|
59
|
+
elapsed_seconds = Time.now - start_time
|
|
60
|
+
return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
def self.build_event(name, data, duration_ms)
|
|
46
67
|
return nil unless data.is_a?(Hash)
|
|
47
68
|
|
data/lib/dead_bro/client.rb
CHANGED
|
@@ -37,6 +37,29 @@ module DeadBro
|
|
|
37
37
|
nil
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def post_job_stats(payload)
|
|
41
|
+
return if @configuration.api_key.nil?
|
|
42
|
+
return unless @configuration.enabled
|
|
43
|
+
return unless @configuration.job_queue_monitoring_enabled
|
|
44
|
+
|
|
45
|
+
# Check circuit breaker before making request
|
|
46
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
47
|
+
if @circuit_breaker.state == :open
|
|
48
|
+
# Check if we should attempt a reset to half-open state
|
|
49
|
+
if @circuit_breaker.should_attempt_reset?
|
|
50
|
+
@circuit_breaker.transition_to_half_open!
|
|
51
|
+
else
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Make the HTTP request (async) to jobs endpoint
|
|
58
|
+
make_job_stats_request(payload, @configuration.api_key)
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
40
63
|
private
|
|
41
64
|
|
|
42
65
|
def create_circuit_breaker
|
|
@@ -99,6 +122,56 @@ module DeadBro
|
|
|
99
122
|
nil
|
|
100
123
|
end
|
|
101
124
|
|
|
125
|
+
def make_job_stats_request(payload, api_key)
|
|
126
|
+
use_staging = ENV["USE_STAGING_ENDPOINT"] && !ENV["USE_STAGING_ENDPOINT"].empty?
|
|
127
|
+
production_url = use_staging ? "https://deadbro.aberatii.com/apm/v1/jobs" : "https://www.deadbro.com/apm/v1/jobs"
|
|
128
|
+
endpoint_url = @configuration.ruby_dev ? "http://localhost:3100/apm/v1/jobs" : production_url
|
|
129
|
+
uri = URI.parse(endpoint_url)
|
|
130
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
131
|
+
http.use_ssl = (uri.scheme == "https")
|
|
132
|
+
http.open_timeout = @configuration.open_timeout
|
|
133
|
+
http.read_timeout = @configuration.read_timeout
|
|
134
|
+
|
|
135
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
136
|
+
request["Content-Type"] = "application/json"
|
|
137
|
+
request["Authorization"] = "Bearer #{api_key}"
|
|
138
|
+
body = {payload: payload, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id}
|
|
139
|
+
request.body = JSON.dump(body)
|
|
140
|
+
|
|
141
|
+
# Fire-and-forget using a short-lived thread to avoid blocking
|
|
142
|
+
Thread.new do
|
|
143
|
+
response = http.request(request)
|
|
144
|
+
|
|
145
|
+
if response
|
|
146
|
+
# Update circuit breaker based on response
|
|
147
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
148
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
149
|
+
@circuit_breaker.send(:on_success)
|
|
150
|
+
else
|
|
151
|
+
@circuit_breaker.send(:on_failure)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
elsif @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
155
|
+
# Treat nil response as failure for circuit breaker
|
|
156
|
+
@circuit_breaker.send(:on_failure)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
response
|
|
160
|
+
rescue Timeout::Error
|
|
161
|
+
# Update circuit breaker on timeout
|
|
162
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
163
|
+
@circuit_breaker.send(:on_failure)
|
|
164
|
+
end
|
|
165
|
+
rescue
|
|
166
|
+
# Update circuit breaker on exception
|
|
167
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
168
|
+
@circuit_breaker.send(:on_failure)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
|
|
102
175
|
def log_debug(message)
|
|
103
176
|
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
104
177
|
Rails.logger.debug(message)
|
|
@@ -5,7 +5,8 @@ module DeadBro
|
|
|
5
5
|
attr_accessor :api_key, :open_timeout, :read_timeout, :enabled, :ruby_dev, :memory_tracking_enabled,
|
|
6
6
|
:allocation_tracking_enabled, :circuit_breaker_enabled, :circuit_breaker_failure_threshold, :circuit_breaker_recovery_timeout,
|
|
7
7
|
:circuit_breaker_retry_timeout, :sample_rate, :excluded_controllers, :excluded_jobs,
|
|
8
|
-
:exclusive_controllers, :exclusive_jobs, :deploy_id, :slow_query_threshold_ms, :explain_analyze_enabled
|
|
8
|
+
:exclusive_controllers, :exclusive_jobs, :deploy_id, :slow_query_threshold_ms, :explain_analyze_enabled,
|
|
9
|
+
:job_queue_monitoring_enabled
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
12
|
@api_key = nil
|
|
@@ -28,6 +29,7 @@ module DeadBro
|
|
|
28
29
|
@deploy_id = resolve_deploy_id
|
|
29
30
|
@slow_query_threshold_ms = 500 # Default: 500ms
|
|
30
31
|
@explain_analyze_enabled = false # Enable EXPLAIN ANALYZE for slow queries by default
|
|
32
|
+
@job_queue_monitoring_enabled = false # Disabled by default
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def resolve_deploy_id
|
|
@@ -6,6 +6,8 @@ require "net/http"
|
|
|
6
6
|
module DeadBro
|
|
7
7
|
module HttpInstrumentation
|
|
8
8
|
EVENT_NAME = "outgoing.http"
|
|
9
|
+
THREAD_LOCAL_KEY = :dead_bro_http_events
|
|
10
|
+
MAX_TRACKED_EVENTS = 1000
|
|
9
11
|
|
|
10
12
|
def self.install!(client: Client.new)
|
|
11
13
|
install_net_http!(client)
|
|
@@ -52,8 +54,8 @@ module DeadBro
|
|
|
52
54
|
exception: error && error.class.name
|
|
53
55
|
}
|
|
54
56
|
# Accumulate per-request; only send with controller metric
|
|
55
|
-
if Thread.current[
|
|
56
|
-
Thread.current[
|
|
57
|
+
if Thread.current[THREAD_LOCAL_KEY] && should_continue_tracking?
|
|
58
|
+
Thread.current[THREAD_LOCAL_KEY] << payload
|
|
57
59
|
end
|
|
58
60
|
end
|
|
59
61
|
rescue
|
|
@@ -96,8 +98,8 @@ module DeadBro
|
|
|
96
98
|
duration_ms: duration_ms
|
|
97
99
|
}
|
|
98
100
|
# Accumulate per-request; only send with controller metric
|
|
99
|
-
if Thread.current[
|
|
100
|
-
Thread.current[
|
|
101
|
+
if Thread.current[THREAD_LOCAL_KEY] && should_continue_tracking?
|
|
102
|
+
Thread.current[THREAD_LOCAL_KEY] << payload
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
rescue
|
|
@@ -109,5 +111,23 @@ module DeadBro
|
|
|
109
111
|
::Typhoeus::Request.prepend(mod) unless ::Typhoeus::Request.ancestors.include?(mod)
|
|
110
112
|
rescue
|
|
111
113
|
end
|
|
114
|
+
|
|
115
|
+
# Check if we should continue tracking based on count and time limits
|
|
116
|
+
def self.should_continue_tracking?
|
|
117
|
+
events = Thread.current[THREAD_LOCAL_KEY]
|
|
118
|
+
return false unless events
|
|
119
|
+
|
|
120
|
+
# Check count limit
|
|
121
|
+
return false if events.length >= MAX_TRACKED_EVENTS
|
|
122
|
+
|
|
123
|
+
# Check time limit
|
|
124
|
+
start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
125
|
+
if start_time
|
|
126
|
+
elapsed_seconds = Time.now - start_time
|
|
127
|
+
return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
true
|
|
131
|
+
end
|
|
112
132
|
end
|
|
113
133
|
end
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeadBro
|
|
4
|
+
class JobQueueMonitor
|
|
5
|
+
def initialize(client: DeadBro.client)
|
|
6
|
+
@client = client
|
|
7
|
+
@thread = nil
|
|
8
|
+
@running = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def start
|
|
12
|
+
return if @running
|
|
13
|
+
return unless DeadBro.configuration.job_queue_monitoring_enabled
|
|
14
|
+
return unless DeadBro.configuration.enabled
|
|
15
|
+
|
|
16
|
+
@running = true
|
|
17
|
+
@thread = Thread.new do
|
|
18
|
+
Thread.current.abort_on_exception = false
|
|
19
|
+
loop do
|
|
20
|
+
break unless @running
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
stats = collect_queue_stats
|
|
24
|
+
@client.post_job_stats(stats) if stats
|
|
25
|
+
rescue => e
|
|
26
|
+
log_error("Error collecting job queue stats: #{e.message}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sleep for 60 seconds (1 minute)
|
|
30
|
+
sleep(120)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@thread
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def stop
|
|
38
|
+
@running = false
|
|
39
|
+
@thread&.join(5) # Wait up to 5 seconds for thread to finish
|
|
40
|
+
@thread = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def collect_queue_stats
|
|
46
|
+
stats = {
|
|
47
|
+
timestamp: Time.now.utc.iso8601,
|
|
48
|
+
queue_system: detect_queue_system,
|
|
49
|
+
environment: Rails.env,
|
|
50
|
+
queues: {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case stats[:queue_system]
|
|
54
|
+
when :sidekiq
|
|
55
|
+
stats[:queues] = collect_sidekiq_stats
|
|
56
|
+
when :solid_queue
|
|
57
|
+
stats[:queues] = collect_solid_queue_stats
|
|
58
|
+
when :delayed_job
|
|
59
|
+
stats[:queues] = collect_delayed_job_stats
|
|
60
|
+
when :good_job
|
|
61
|
+
stats[:queues] = collect_good_job_stats
|
|
62
|
+
else
|
|
63
|
+
return nil # Unknown queue system, don't send stats
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
stats
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def detect_queue_system
|
|
70
|
+
return :sidekiq if defined?(Sidekiq)
|
|
71
|
+
return :solid_queue if defined?(SolidQueue)
|
|
72
|
+
return :delayed_job if defined?(Delayed::Job)
|
|
73
|
+
return :good_job if defined?(GoodJob)
|
|
74
|
+
:unknown
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collect_sidekiq_stats
|
|
78
|
+
return {} unless defined?(Sidekiq)
|
|
79
|
+
|
|
80
|
+
stats = {
|
|
81
|
+
total_queued: 0,
|
|
82
|
+
total_busy: 0,
|
|
83
|
+
queues: {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
# Get queue sizes - try to access Queue class (will trigger autoload if needed)
|
|
88
|
+
begin
|
|
89
|
+
queue_class = Sidekiq.const_get(:Queue)
|
|
90
|
+
if queue_class.respond_to?(:all)
|
|
91
|
+
queue_class.all.each do |queue|
|
|
92
|
+
queue_name = queue.name
|
|
93
|
+
size = queue.size
|
|
94
|
+
stats[:queues][queue_name] = {
|
|
95
|
+
queued: size,
|
|
96
|
+
busy: 0,
|
|
97
|
+
scheduled: 0,
|
|
98
|
+
retries: 0
|
|
99
|
+
}
|
|
100
|
+
stats[:total_queued] += size
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
rescue NameError => e
|
|
104
|
+
log_error("Sidekiq::Queue not available: #{e.message}")
|
|
105
|
+
rescue => e
|
|
106
|
+
log_error("Error accessing Sidekiq::Queue: #{e.message}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get busy workers
|
|
110
|
+
begin
|
|
111
|
+
workers_class = Sidekiq.const_get(:Workers)
|
|
112
|
+
workers = workers_class.new
|
|
113
|
+
workers.each do |process_id, thread_id, work|
|
|
114
|
+
next unless work
|
|
115
|
+
queue_name = work["queue"] || "default"
|
|
116
|
+
stats[:queues][queue_name] ||= { queued: 0, busy: 0, scheduled: 0, retries: 0 }
|
|
117
|
+
stats[:queues][queue_name][:busy] += 1
|
|
118
|
+
stats[:total_busy] += 1
|
|
119
|
+
end
|
|
120
|
+
rescue NameError
|
|
121
|
+
# Workers class not available, try fallback
|
|
122
|
+
if Sidekiq.respond_to?(:workers)
|
|
123
|
+
# Fallback for older Sidekiq versions
|
|
124
|
+
begin
|
|
125
|
+
workers = Sidekiq.workers
|
|
126
|
+
if workers.respond_to?(:each)
|
|
127
|
+
workers.each do |worker|
|
|
128
|
+
queue_name = worker.respond_to?(:queue) ? worker.queue : "default"
|
|
129
|
+
stats[:queues][queue_name] ||= { queued: 0, busy: 0, scheduled: 0, retries: 0 }
|
|
130
|
+
stats[:queues][queue_name][:busy] += 1
|
|
131
|
+
stats[:total_busy] += 1
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
rescue => e
|
|
135
|
+
log_error("Error getting Sidekiq workers (fallback): #{e.message}")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
rescue => e
|
|
139
|
+
log_error("Error getting Sidekiq workers: #{e.message}")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get scheduled jobs
|
|
143
|
+
begin
|
|
144
|
+
scheduled_set_class = Sidekiq.const_get(:ScheduledSet)
|
|
145
|
+
scheduled_set = scheduled_set_class.new
|
|
146
|
+
stats[:total_scheduled] = scheduled_set.size
|
|
147
|
+
rescue NameError
|
|
148
|
+
# ScheduledSet not available, skip
|
|
149
|
+
rescue => e
|
|
150
|
+
log_error("Error getting Sidekiq scheduled jobs: #{e.message}")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get retries
|
|
154
|
+
begin
|
|
155
|
+
retry_set_class = Sidekiq.const_get(:RetrySet)
|
|
156
|
+
retry_set = retry_set_class.new
|
|
157
|
+
stats[:total_retries] = retry_set.size
|
|
158
|
+
rescue NameError
|
|
159
|
+
# RetrySet not available, skip
|
|
160
|
+
rescue => e
|
|
161
|
+
log_error("Error getting Sidekiq retries: #{e.message}")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get dead jobs
|
|
165
|
+
begin
|
|
166
|
+
dead_set_class = Sidekiq.const_get(:DeadSet)
|
|
167
|
+
dead_set = dead_set_class.new
|
|
168
|
+
stats[:total_dead] = dead_set.size
|
|
169
|
+
rescue NameError
|
|
170
|
+
# DeadSet not available, skip
|
|
171
|
+
rescue => e
|
|
172
|
+
log_error("Error getting Sidekiq dead jobs: #{e.message}")
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get process info
|
|
176
|
+
begin
|
|
177
|
+
process_set_class = Sidekiq.const_get(:ProcessSet)
|
|
178
|
+
process_set = process_set_class.new
|
|
179
|
+
stats[:processes] = process_set.size
|
|
180
|
+
rescue NameError
|
|
181
|
+
# ProcessSet not available, skip
|
|
182
|
+
rescue => e
|
|
183
|
+
log_error("Error getting Sidekiq processes: #{e.message}")
|
|
184
|
+
end
|
|
185
|
+
rescue => e
|
|
186
|
+
log_error("Error collecting Sidekiq stats: #{e.message}")
|
|
187
|
+
log_error("Backtrace: #{e.backtrace.first(5).join("\n")}")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
stats
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def collect_solid_queue_stats
|
|
194
|
+
return {} unless defined?(SolidQueue)
|
|
195
|
+
|
|
196
|
+
stats = {
|
|
197
|
+
total_queued: 0,
|
|
198
|
+
total_busy: 0,
|
|
199
|
+
queues: {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
begin
|
|
203
|
+
# Solid Queue uses ActiveJob and stores jobs in a database table
|
|
204
|
+
if defined?(ActiveRecord) && ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?("solid_queue_jobs")
|
|
205
|
+
# Get queued jobs grouped by queue
|
|
206
|
+
result = ActiveRecord::Base.connection.execute(
|
|
207
|
+
"SELECT queue_name, COUNT(*) as count FROM solid_queue_jobs WHERE finished_at IS NULL GROUP BY queue_name"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
parse_query_result(result).each do |row|
|
|
211
|
+
queue_name = (row["queue_name"] || row[:queue_name] || "default").to_s
|
|
212
|
+
count = (row["count"] || row[:count] || 0).to_i
|
|
213
|
+
stats[:queues][queue_name] = {
|
|
214
|
+
queued: count,
|
|
215
|
+
busy: 0,
|
|
216
|
+
scheduled: 0,
|
|
217
|
+
retries: 0
|
|
218
|
+
}
|
|
219
|
+
stats[:total_queued] += count
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Get busy jobs (claimed but not finished)
|
|
223
|
+
result = ActiveRecord::Base.connection.execute(
|
|
224
|
+
"SELECT queue_name, COUNT(*) as count FROM solid_queue_jobs WHERE finished_at IS NULL AND claimed_at IS NOT NULL GROUP BY queue_name"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
parse_query_result(result).each do |row|
|
|
228
|
+
queue_name = (row["queue_name"] || row[:queue_name] || "default").to_s
|
|
229
|
+
count = (row["count"] || row[:count] || 0).to_i
|
|
230
|
+
stats[:queues][queue_name] ||= { queued: 0, busy: 0, scheduled: 0, retries: 0 }
|
|
231
|
+
stats[:queues][queue_name][:busy] = count
|
|
232
|
+
stats[:total_busy] += count
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Get scheduled jobs
|
|
236
|
+
result = ActiveRecord::Base.connection.execute(
|
|
237
|
+
"SELECT COUNT(*) as count FROM solid_queue_jobs WHERE scheduled_at > NOW()"
|
|
238
|
+
)
|
|
239
|
+
scheduled_count = parse_query_result(result).first
|
|
240
|
+
stats[:total_scheduled] = (scheduled_count&.dig("count") || scheduled_count&.dig(:count) || 0).to_i
|
|
241
|
+
|
|
242
|
+
# Get failed jobs
|
|
243
|
+
if ActiveRecord::Base.connection.table_exists?("solid_queue_failed_jobs")
|
|
244
|
+
result = ActiveRecord::Base.connection.execute(
|
|
245
|
+
"SELECT COUNT(*) as count FROM solid_queue_failed_jobs"
|
|
246
|
+
)
|
|
247
|
+
failed_count = parse_query_result(result).first
|
|
248
|
+
stats[:total_failed] = (failed_count&.dig("count") || failed_count&.dig(:count) || 0).to_i
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
rescue => e
|
|
252
|
+
log_error("Error collecting Solid Queue stats: #{e.message}")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
stats
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def collect_delayed_job_stats
|
|
259
|
+
return {} unless defined?(Delayed::Job)
|
|
260
|
+
|
|
261
|
+
stats = {
|
|
262
|
+
total_queued: 0,
|
|
263
|
+
total_busy: 0,
|
|
264
|
+
queues: {}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
begin
|
|
268
|
+
# Delayed Job uses a single table
|
|
269
|
+
if defined?(ActiveRecord) && ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?("delayed_jobs")
|
|
270
|
+
# Get queued jobs
|
|
271
|
+
queued = Delayed::Job.where("locked_at IS NULL AND attempts < max_attempts").count
|
|
272
|
+
stats[:total_queued] = queued
|
|
273
|
+
stats[:queues]["default"] = {
|
|
274
|
+
queued: queued,
|
|
275
|
+
busy: 0,
|
|
276
|
+
scheduled: 0,
|
|
277
|
+
retries: 0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Get busy jobs (locked)
|
|
281
|
+
busy = Delayed::Job.where("locked_at IS NOT NULL AND locked_by IS NOT NULL").count
|
|
282
|
+
stats[:total_busy] = busy
|
|
283
|
+
stats[:queues]["default"][:busy] = busy
|
|
284
|
+
|
|
285
|
+
# Get failed jobs
|
|
286
|
+
failed = Delayed::Job.where("attempts >= max_attempts").count
|
|
287
|
+
stats[:total_failed] = failed
|
|
288
|
+
end
|
|
289
|
+
rescue => e
|
|
290
|
+
log_error("Error collecting Delayed Job stats: #{e.message}")
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
stats
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def collect_good_job_stats
|
|
297
|
+
return {} unless defined?(GoodJob)
|
|
298
|
+
|
|
299
|
+
stats = {
|
|
300
|
+
total_queued: 0,
|
|
301
|
+
total_busy: 0,
|
|
302
|
+
queues: {}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
begin
|
|
306
|
+
# Good Job uses ActiveJob and stores jobs in a database table
|
|
307
|
+
if defined?(ActiveRecord) && ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?("good_jobs")
|
|
308
|
+
# Get queued jobs grouped by queue
|
|
309
|
+
result = ActiveRecord::Base.connection.execute(
|
|
310
|
+
"SELECT queue_name, COUNT(*) as count FROM good_jobs WHERE finished_at IS NULL GROUP BY queue_name"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
parse_query_result(result).each do |row|
|
|
314
|
+
queue_name = (row["queue_name"] || row[:queue_name] || "default").to_s
|
|
315
|
+
count = (row["count"] || row[:count] || 0).to_i
|
|
316
|
+
stats[:queues][queue_name] = {
|
|
317
|
+
queued: count,
|
|
318
|
+
busy: 0,
|
|
319
|
+
scheduled: 0,
|
|
320
|
+
retries: 0
|
|
321
|
+
}
|
|
322
|
+
stats[:total_queued] += count
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Get busy jobs (running)
|
|
326
|
+
result = ActiveRecord::Base.connection.execute(
|
|
327
|
+
"SELECT queue_name, COUNT(*) as count FROM good_jobs WHERE finished_at IS NULL AND performed_at IS NOT NULL GROUP BY queue_name"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
parse_query_result(result).each do |row|
|
|
331
|
+
queue_name = (row["queue_name"] || row[:queue_name] || "default").to_s
|
|
332
|
+
count = (row["count"] || row[:count] || 0).to_i
|
|
333
|
+
stats[:queues][queue_name] ||= { queued: 0, busy: 0, scheduled: 0, retries: 0 }
|
|
334
|
+
stats[:queues][queue_name][:busy] = count
|
|
335
|
+
stats[:total_busy] += count
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Get scheduled jobs
|
|
339
|
+
result = ActiveRecord::Base.connection.execute(
|
|
340
|
+
"SELECT COUNT(*) as count FROM good_jobs WHERE scheduled_at > NOW()"
|
|
341
|
+
)
|
|
342
|
+
scheduled_count = parse_query_result(result).first
|
|
343
|
+
stats[:total_scheduled] = (scheduled_count&.dig("count") || scheduled_count&.dig(:count) || 0).to_i
|
|
344
|
+
|
|
345
|
+
# Get failed jobs
|
|
346
|
+
result = ActiveRecord::Base.connection.execute(
|
|
347
|
+
"SELECT COUNT(*) as count FROM good_jobs WHERE finished_at IS NOT NULL AND error IS NOT NULL"
|
|
348
|
+
)
|
|
349
|
+
failed_count = parse_query_result(result).first
|
|
350
|
+
stats[:total_failed] = (failed_count&.dig("count") || failed_count&.dig(:count) || 0).to_i
|
|
351
|
+
end
|
|
352
|
+
rescue => e
|
|
353
|
+
log_error("Error collecting Good Job stats: #{e.message}")
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
stats
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def parse_query_result(result)
|
|
360
|
+
# Handle different database adapter result formats
|
|
361
|
+
if result.respond_to?(:each)
|
|
362
|
+
# PostgreSQL PG::Result or similar
|
|
363
|
+
if result.respond_to?(:values)
|
|
364
|
+
# Convert to array of hashes
|
|
365
|
+
columns = result.fields rescue result.column_names rescue []
|
|
366
|
+
result.values.map do |row|
|
|
367
|
+
columns.each_with_index.each_with_object({}) do |(col, idx), hash|
|
|
368
|
+
hash[col.to_s] = row[idx]
|
|
369
|
+
hash[col.to_sym] = row[idx]
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
elsif result.is_a?(Array)
|
|
373
|
+
# Already an array
|
|
374
|
+
result
|
|
375
|
+
else
|
|
376
|
+
# Try to convert to array
|
|
377
|
+
result.to_a
|
|
378
|
+
end
|
|
379
|
+
else
|
|
380
|
+
[]
|
|
381
|
+
end
|
|
382
|
+
rescue => e
|
|
383
|
+
log_error("Error parsing query result: #{e.message}")
|
|
384
|
+
[]
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def log_error(message)
|
|
388
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
389
|
+
Rails.logger.error("[DeadBro::JobQueueMonitor] #{message}")
|
|
390
|
+
else
|
|
391
|
+
$stderr.puts("[DeadBro::JobQueueMonitor] #{message}")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
@@ -7,6 +7,10 @@ module DeadBro
|
|
|
7
7
|
ActiveSupport::Notifications.subscribe("perform_start.active_job") do |name, started, finished, _unique_id, data|
|
|
8
8
|
# Clear logs for this job
|
|
9
9
|
DeadBro.logger.clear
|
|
10
|
+
|
|
11
|
+
# Set tracking start time once for all subscribers (before starting any tracking)
|
|
12
|
+
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
|
|
13
|
+
|
|
10
14
|
DeadBro::SqlSubscriber.start_request_tracking
|
|
11
15
|
|
|
12
16
|
# Start lightweight memory tracking for this job
|
|
@@ -28,6 +28,14 @@ module DeadBro
|
|
|
28
28
|
|
|
29
29
|
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
30
30
|
|
|
31
|
+
# Ensure tracking was started (fallback if perform_start.active_job didn't fire)
|
|
32
|
+
# This handles job backends that don't emit perform_start events
|
|
33
|
+
unless Thread.current[DeadBro::SqlSubscriber::THREAD_LOCAL_KEY]
|
|
34
|
+
DeadBro.logger.clear
|
|
35
|
+
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
|
|
36
|
+
DeadBro::SqlSubscriber.start_request_tracking
|
|
37
|
+
end
|
|
38
|
+
|
|
31
39
|
# Get SQL queries executed during this job
|
|
32
40
|
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
33
41
|
|
|
@@ -96,6 +104,14 @@ module DeadBro
|
|
|
96
104
|
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
97
105
|
exception = data[:exception_object]
|
|
98
106
|
|
|
107
|
+
# Ensure tracking was started (fallback if perform_start.active_job didn't fire)
|
|
108
|
+
# This handles job backends that don't emit perform_start events
|
|
109
|
+
unless Thread.current[DeadBro::SqlSubscriber::THREAD_LOCAL_KEY]
|
|
110
|
+
DeadBro.logger.clear
|
|
111
|
+
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
|
|
112
|
+
DeadBro::SqlSubscriber.start_request_tracking
|
|
113
|
+
end
|
|
114
|
+
|
|
99
115
|
# Get SQL queries executed during this job
|
|
100
116
|
sql_queries = DeadBro::SqlSubscriber.stop_request_tracking
|
|
101
117
|
|
data/lib/dead_bro/railtie.rb
CHANGED
|
@@ -55,6 +55,13 @@ if defined?(Rails) && defined?(Rails::Railtie)
|
|
|
55
55
|
DeadBro::JobSqlTrackingMiddleware.subscribe!
|
|
56
56
|
DeadBro::JobSubscriber.subscribe!(client: shared_client)
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
# Start job queue monitoring if enabled
|
|
60
|
+
if DeadBro.configuration.job_queue_monitoring_enabled
|
|
61
|
+
require "dead_bro/job_queue_monitor"
|
|
62
|
+
DeadBro.job_queue_monitor = DeadBro::JobQueueMonitor.new(client: shared_client)
|
|
63
|
+
DeadBro.job_queue_monitor.start
|
|
64
|
+
end
|
|
58
65
|
rescue
|
|
59
66
|
# Never raise in Railtie init
|
|
60
67
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module DeadBro
|
|
4
4
|
class RedisSubscriber
|
|
5
5
|
THREAD_LOCAL_KEY = :dead_bro_redis_events
|
|
6
|
+
MAX_TRACKED_EVENTS = 1000
|
|
6
7
|
|
|
7
8
|
def self.subscribe!
|
|
8
9
|
install_redis_instrumentation!
|
|
@@ -86,7 +87,7 @@ module DeadBro
|
|
|
86
87
|
error: error ? error.class.name : nil
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
90
|
+
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] && RedisSubscriber.should_continue_tracking?
|
|
90
91
|
Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
|
|
91
92
|
end
|
|
92
93
|
rescue
|
|
@@ -114,7 +115,7 @@ module DeadBro
|
|
|
114
115
|
db: safe_db(@db)
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
118
|
+
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] && RedisSubscriber.should_continue_tracking?
|
|
118
119
|
Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
|
|
119
120
|
end
|
|
120
121
|
rescue
|
|
@@ -142,7 +143,7 @@ module DeadBro
|
|
|
142
143
|
db: safe_db(@db)
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
|
|
146
|
+
if Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] && RedisSubscriber.should_continue_tracking?
|
|
146
147
|
Thread.current[RedisSubscriber::THREAD_LOCAL_KEY] << event
|
|
147
148
|
end
|
|
148
149
|
rescue
|
|
@@ -201,7 +202,9 @@ module DeadBro
|
|
|
201
202
|
next unless Thread.current[THREAD_LOCAL_KEY]
|
|
202
203
|
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
203
204
|
event = build_event(name, data, duration_ms)
|
|
204
|
-
|
|
205
|
+
if event && should_continue_tracking?
|
|
206
|
+
Thread.current[THREAD_LOCAL_KEY] << event
|
|
207
|
+
end
|
|
205
208
|
end
|
|
206
209
|
rescue
|
|
207
210
|
end
|
|
@@ -218,6 +221,24 @@ module DeadBro
|
|
|
218
221
|
events || []
|
|
219
222
|
end
|
|
220
223
|
|
|
224
|
+
# Check if we should continue tracking based on count and time limits
|
|
225
|
+
def self.should_continue_tracking?
|
|
226
|
+
events = Thread.current[THREAD_LOCAL_KEY]
|
|
227
|
+
return false unless events
|
|
228
|
+
|
|
229
|
+
# Check count limit
|
|
230
|
+
return false if events.length >= MAX_TRACKED_EVENTS
|
|
231
|
+
|
|
232
|
+
# Check time limit
|
|
233
|
+
start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
234
|
+
if start_time
|
|
235
|
+
elapsed_seconds = Time.now - start_time
|
|
236
|
+
return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
true
|
|
240
|
+
end
|
|
241
|
+
|
|
221
242
|
def self.build_event(name, data, duration_ms)
|
|
222
243
|
cmd = extract_command(data)
|
|
223
244
|
{
|
|
@@ -14,6 +14,25 @@ module DeadBro
|
|
|
14
14
|
THREAD_LOCAL_ALLOC_RESULTS_KEY = :dead_bro_sql_alloc_results
|
|
15
15
|
THREAD_LOCAL_BACKTRACE_KEY = :dead_bro_sql_backtraces
|
|
16
16
|
THREAD_LOCAL_EXPLAIN_PENDING_KEY = :dead_bro_explain_pending
|
|
17
|
+
MAX_TRACKED_QUERIES = 1000
|
|
18
|
+
|
|
19
|
+
# Check if we should continue tracking based on count and time limits
|
|
20
|
+
def self.should_continue_tracking?(thread_local_key, max_count)
|
|
21
|
+
events = Thread.current[thread_local_key]
|
|
22
|
+
return false unless events
|
|
23
|
+
|
|
24
|
+
# Check count limit
|
|
25
|
+
return false if events.length >= max_count
|
|
26
|
+
|
|
27
|
+
# Check time limit
|
|
28
|
+
start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
29
|
+
if start_time
|
|
30
|
+
elapsed_seconds = Time.now - start_time
|
|
31
|
+
return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
17
36
|
|
|
18
37
|
def self.subscribe!
|
|
19
38
|
# Subscribe with a start/finish listener to measure allocations per query
|
|
@@ -63,8 +82,10 @@ module DeadBro
|
|
|
63
82
|
start_explain_analyze_background(original_sql, data[:connection_id], query_info, binds)
|
|
64
83
|
end
|
|
65
84
|
|
|
66
|
-
# Add to thread-local storage
|
|
67
|
-
|
|
85
|
+
# Add to thread-local storage, but only if we haven't exceeded the limits
|
|
86
|
+
if should_continue_tracking?(THREAD_LOCAL_KEY, MAX_TRACKED_QUERIES)
|
|
87
|
+
Thread.current[THREAD_LOCAL_KEY] << query_info
|
|
88
|
+
end
|
|
68
89
|
end
|
|
69
90
|
end
|
|
70
91
|
|
|
@@ -354,8 +375,12 @@ module DeadBro
|
|
|
354
375
|
# Fallback to string representation
|
|
355
376
|
result.to_s
|
|
356
377
|
when "mysql", "mysql2", "trilogy"
|
|
357
|
-
# MySQL returns
|
|
358
|
-
if result.
|
|
378
|
+
# MySQL returns Mysql2::Result object which needs to be converted to array
|
|
379
|
+
if result.respond_to?(:to_a)
|
|
380
|
+
# Convert Mysql2::Result to array of hashes
|
|
381
|
+
rows = result.to_a
|
|
382
|
+
rows.map { |row| row.is_a?(Hash) ? row.values.join(" | ") : row.to_s }.join("\n")
|
|
383
|
+
elsif result.is_a?(Array)
|
|
359
384
|
result.map { |row| row.is_a?(Hash) ? row.values.join(" | ") : row.to_s }.join("\n")
|
|
360
385
|
else
|
|
361
386
|
result.to_s
|
|
@@ -42,6 +42,9 @@ module DeadBro
|
|
|
42
42
|
|
|
43
43
|
# Start outgoing HTTP accumulation for this request
|
|
44
44
|
Thread.current[:dead_bro_http_events] = []
|
|
45
|
+
|
|
46
|
+
# Set tracking start time once for all subscribers (before starting any tracking)
|
|
47
|
+
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = Time.now
|
|
45
48
|
|
|
46
49
|
@app.call(env)
|
|
47
50
|
ensure
|
|
@@ -71,8 +74,9 @@ module DeadBro
|
|
|
71
74
|
Thread.current[:dead_bro_lightweight_memory] = nil
|
|
72
75
|
end
|
|
73
76
|
|
|
74
|
-
# Clean up HTTP events
|
|
77
|
+
# Clean up HTTP events and tracking start time
|
|
75
78
|
Thread.current[:dead_bro_http_events] = nil
|
|
79
|
+
Thread.current[DeadBro::TRACKING_START_TIME_KEY] = nil
|
|
76
80
|
end
|
|
77
81
|
end
|
|
78
82
|
end
|
data/lib/dead_bro/version.rb
CHANGED
|
@@ -10,6 +10,7 @@ module DeadBro
|
|
|
10
10
|
RENDER_COLLECTION_EVENT = "render_collection.action_view"
|
|
11
11
|
|
|
12
12
|
THREAD_LOCAL_KEY = :dead_bro_view_events
|
|
13
|
+
MAX_TRACKED_EVENTS = 1000
|
|
13
14
|
|
|
14
15
|
def self.subscribe!(client: Client.new)
|
|
15
16
|
# Track template rendering
|
|
@@ -78,11 +79,29 @@ module DeadBro
|
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
def self.add_view_event(view_info)
|
|
81
|
-
if Thread.current[THREAD_LOCAL_KEY]
|
|
82
|
+
if Thread.current[THREAD_LOCAL_KEY] && should_continue_tracking?
|
|
82
83
|
Thread.current[THREAD_LOCAL_KEY] << view_info
|
|
83
84
|
end
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
# Check if we should continue tracking based on count and time limits
|
|
88
|
+
def self.should_continue_tracking?
|
|
89
|
+
events = Thread.current[THREAD_LOCAL_KEY]
|
|
90
|
+
return false unless events
|
|
91
|
+
|
|
92
|
+
# Check count limit
|
|
93
|
+
return false if events.length >= MAX_TRACKED_EVENTS
|
|
94
|
+
|
|
95
|
+
# Check time limit
|
|
96
|
+
start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
97
|
+
if start_time
|
|
98
|
+
elapsed_seconds = Time.now - start_time
|
|
99
|
+
return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
86
105
|
def self.safe_identifier(identifier)
|
|
87
106
|
return "" unless identifier.is_a?(String)
|
|
88
107
|
|
data/lib/dead_bro.rb
CHANGED
|
@@ -18,6 +18,7 @@ module DeadBro
|
|
|
18
18
|
autoload :MemoryHelpers, "dead_bro/memory_helpers"
|
|
19
19
|
autoload :JobSubscriber, "dead_bro/job_subscriber"
|
|
20
20
|
autoload :JobSqlTrackingMiddleware, "dead_bro/job_sql_tracking_middleware"
|
|
21
|
+
autoload :JobQueueMonitor, "dead_bro/job_queue_monitor"
|
|
21
22
|
autoload :Logger, "dead_bro/logger"
|
|
22
23
|
begin
|
|
23
24
|
require "dead_bro/railtie"
|
|
@@ -66,4 +67,18 @@ module DeadBro
|
|
|
66
67
|
ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
|
|
67
68
|
end
|
|
68
69
|
end
|
|
70
|
+
|
|
71
|
+
# Returns the job queue monitor instance
|
|
72
|
+
def self.job_queue_monitor
|
|
73
|
+
@job_queue_monitor
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Sets the job queue monitor instance
|
|
77
|
+
def self.job_queue_monitor=(monitor)
|
|
78
|
+
@job_queue_monitor = monitor
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Shared constant for tracking start time (used by all subscribers)
|
|
82
|
+
TRACKING_START_TIME_KEY = :dead_bro_tracking_start_time
|
|
83
|
+
MAX_TRACKING_DURATION_SECONDS = 3600 # 1 hour
|
|
69
84
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dead_bro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Emanuel Comsa
|
|
@@ -27,6 +27,7 @@ files:
|
|
|
27
27
|
- lib/dead_bro/configuration.rb
|
|
28
28
|
- lib/dead_bro/error_middleware.rb
|
|
29
29
|
- lib/dead_bro/http_instrumentation.rb
|
|
30
|
+
- lib/dead_bro/job_queue_monitor.rb
|
|
30
31
|
- lib/dead_bro/job_sql_tracking_middleware.rb
|
|
31
32
|
- lib/dead_bro/job_subscriber.rb
|
|
32
33
|
- lib/dead_bro/lightweight_memory_tracker.rb
|
|
@@ -60,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
60
61
|
- !ruby/object:Gem::Version
|
|
61
62
|
version: '0'
|
|
62
63
|
requirements: []
|
|
63
|
-
rubygems_version:
|
|
64
|
+
rubygems_version: 4.0.3
|
|
64
65
|
specification_version: 4
|
|
65
66
|
summary: Minimal APM for Rails apps.
|
|
66
67
|
test_files: []
|