rails_trace_viewer 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +224 -0
- data/app/channels/rails_trace_viewer/trace_channel.rb +7 -0
- data/app/controllers/rails_trace_viewer/traces_controller.rb +6 -0
- data/app/views/rails_trace_viewer/traces/show.html.erb +410 -0
- data/config/routes.rb +3 -0
- data/lib/rails_trace_viewer/broadcaster.rb +13 -0
- data/lib/rails_trace_viewer/collector.rb +151 -0
- data/lib/rails_trace_viewer/engine.rb +32 -0
- data/lib/rails_trace_viewer/job_link_registry.rb +42 -0
- data/lib/rails_trace_viewer/route_resolver.rb +20 -0
- data/lib/rails_trace_viewer/subscribers/active_job_subscriber.rb +72 -0
- data/lib/rails_trace_viewer/subscribers/controller_subscriber.rb +51 -0
- data/lib/rails_trace_viewer/subscribers/method_subscriber.rb +59 -0
- data/lib/rails_trace_viewer/subscribers/sidekiq_subscriber.rb +111 -0
- data/lib/rails_trace_viewer/subscribers/sql_subscriber.rb +43 -0
- data/lib/rails_trace_viewer/subscribers/view_subscriber.rb +34 -0
- data/lib/rails_trace_viewer/trace_context.rb +56 -0
- data/lib/rails_trace_viewer/version.rb +5 -0
- data/lib/rails_trace_viewer.rb +22 -0
- metadata +125 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
class Collector
|
|
3
|
+
TRACE_TREES = {}
|
|
4
|
+
|
|
5
|
+
@sweeper_started = false
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def start_sweeper!
|
|
10
|
+
return if @sweeper_started
|
|
11
|
+
|
|
12
|
+
@sweeper_started = true
|
|
13
|
+
|
|
14
|
+
Thread.new do
|
|
15
|
+
Thread.current.name = "RailsTraceViewer-OrphanSweeper"
|
|
16
|
+
loop do
|
|
17
|
+
sleep 3
|
|
18
|
+
sweep_all_orphans
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start_trace(trace_id)
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
TRACE_TREES[trace_id] ||= {
|
|
26
|
+
root: nil,
|
|
27
|
+
index: {},
|
|
28
|
+
orphans: {}
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_node(trace_id, node)
|
|
34
|
+
after_broadcasts = []
|
|
35
|
+
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
after_broadcasts = process_node(trace_id, node)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
after_broadcasts.each(&:call)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def process_node(trace_id, node)
|
|
44
|
+
after = []
|
|
45
|
+
tree = TRACE_TREES[trace_id]
|
|
46
|
+
|
|
47
|
+
unless tree
|
|
48
|
+
tree = { root: nil, index: {}, orphans: {} }
|
|
49
|
+
TRACE_TREES[trace_id] = tree
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
id = node[:id]
|
|
53
|
+
parent_id = node[:parent_id]
|
|
54
|
+
|
|
55
|
+
tree[:index][id] = node
|
|
56
|
+
|
|
57
|
+
if parent_id.nil?
|
|
58
|
+
tree[:root] ||= node
|
|
59
|
+
after << -> { Broadcaster.emit(trace_id, node) }
|
|
60
|
+
after.concat attach_waiting_children(tree, trace_id, id, node)
|
|
61
|
+
|
|
62
|
+
else
|
|
63
|
+
parent = tree[:index][parent_id]
|
|
64
|
+
|
|
65
|
+
if parent
|
|
66
|
+
parent[:children] ||= []
|
|
67
|
+
parent[:children] << node
|
|
68
|
+
|
|
69
|
+
after << -> { Broadcaster.emit(trace_id, node) }
|
|
70
|
+
after.concat attach_waiting_children(tree, trace_id, id, node)
|
|
71
|
+
elsif node[:type] == "job_perform"
|
|
72
|
+
after << -> { Broadcaster.emit(trace_id, node) }
|
|
73
|
+
|
|
74
|
+
after.concat attach_waiting_children(tree, trace_id, id, node)
|
|
75
|
+
else
|
|
76
|
+
tree[:orphans][parent_id] ||= []
|
|
77
|
+
tree[:orphans][parent_id] << {
|
|
78
|
+
node: node,
|
|
79
|
+
time: Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
after
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def attach_waiting_children(tree, trace_id, parent_id, parent_node)
|
|
88
|
+
return [] unless tree[:orphans][parent_id]
|
|
89
|
+
|
|
90
|
+
after = []
|
|
91
|
+
|
|
92
|
+
tree[:orphans][parent_id].each do |entry|
|
|
93
|
+
child = entry[:node]
|
|
94
|
+
parent_node[:children] ||= []
|
|
95
|
+
parent_node[:children] << child
|
|
96
|
+
|
|
97
|
+
after << -> { Broadcaster.emit(trace_id, child) }
|
|
98
|
+
|
|
99
|
+
after.concat attach_waiting_children(tree, trace_id, child[:id], child)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
tree[:orphans].delete(parent_id)
|
|
103
|
+
after
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def sweep_all_orphans
|
|
107
|
+
after = []
|
|
108
|
+
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
111
|
+
|
|
112
|
+
TRACE_TREES.each do |trace_id, tree|
|
|
113
|
+
next if tree[:orphans].empty?
|
|
114
|
+
|
|
115
|
+
tree[:orphans].each do |parent_id, entries|
|
|
116
|
+
entries.reject! do |entry|
|
|
117
|
+
if now - entry[:time] > 3
|
|
118
|
+
orphan = entry[:node]
|
|
119
|
+
|
|
120
|
+
after << -> { Broadcaster.emit(trace_id, orphan) }
|
|
121
|
+
true
|
|
122
|
+
else
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
after.each(&:call)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def finalize_trace(trace_id)
|
|
134
|
+
trace = nil
|
|
135
|
+
|
|
136
|
+
@mutex.synchronize do
|
|
137
|
+
tree = TRACE_TREES[trace_id]
|
|
138
|
+
return unless tree
|
|
139
|
+
trace = tree[:root]
|
|
140
|
+
TRACE_TREES.delete(trace_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
ActionCable.server.broadcast(
|
|
144
|
+
"rails_trace_viewer",
|
|
145
|
+
event: "trace_completed",
|
|
146
|
+
trace: trace
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace RailsTraceViewer
|
|
4
|
+
|
|
5
|
+
initializer "rails_trace_viewer.subscribe", after: :load_config_initializers do
|
|
6
|
+
ActiveSupport.on_load(:action_controller) do
|
|
7
|
+
RailsTraceViewer::Subscribers::ControllerSubscriber.attach
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ActiveSupport.on_load(:active_record) do
|
|
11
|
+
RailsTraceViewer::Subscribers::SqlSubscriber.attach
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
ActiveSupport.on_load(:action_view) do
|
|
15
|
+
RailsTraceViewer::Subscribers::ViewSubscriber.attach
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
ActiveSupport.on_load(:active_job) do
|
|
19
|
+
RailsTraceViewer::Subscribers::ActiveJobSubscriber.attach
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Rails.application.reloader.to_prepare do
|
|
23
|
+
RailsTraceViewer::Subscribers::SidekiqSubscriber.attach
|
|
24
|
+
RailsTraceViewer::Subscribers::MethodSubscriber.attach
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
initializer "rails_trace_viewer.start_sweeper" do
|
|
29
|
+
RailsTraceViewer::Collector.start_sweeper!
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "concurrent/map"
|
|
2
|
+
|
|
3
|
+
module RailsTraceViewer
|
|
4
|
+
class JobLinkRegistry
|
|
5
|
+
@store = Concurrent::Map.new
|
|
6
|
+
@pending = Concurrent::Map.new
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def register(job, trace_id:, enqueue_node_id:)
|
|
10
|
+
job_id = job.job_id
|
|
11
|
+
|
|
12
|
+
@store[job_id] = {
|
|
13
|
+
trace_id: trace_id,
|
|
14
|
+
enqueue_node_id: enqueue_node_id
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if @pending[job_id]
|
|
18
|
+
@pending[job_id].each do |callback|
|
|
19
|
+
callback.call(trace_id, enqueue_node_id)
|
|
20
|
+
end
|
|
21
|
+
@pending.delete(job_id)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_perform(job, &block)
|
|
26
|
+
job_id = job.job_id
|
|
27
|
+
|
|
28
|
+
if @store[job_id]
|
|
29
|
+
data = @store[job_id]
|
|
30
|
+
block.call(data[:trace_id], data[:enqueue_node_id])
|
|
31
|
+
else
|
|
32
|
+
@pending[job_id] ||= []
|
|
33
|
+
@pending[job_id] << block
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delete(job)
|
|
38
|
+
@store.delete(job.job_id)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
class RouteResolver
|
|
3
|
+
def self.resolve(path, method)
|
|
4
|
+
recognized = Rails.application.routes.recognize_path(path, method: method.downcase.to_sym)
|
|
5
|
+
|
|
6
|
+
route = Rails.application.routes.routes.find do |r|
|
|
7
|
+
r.defaults[:controller] == recognized[:controller] &&
|
|
8
|
+
r.defaults[:action] == recognized[:action]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
name: route&.name || "(unnamed)",
|
|
13
|
+
verb: method,
|
|
14
|
+
path: path
|
|
15
|
+
}
|
|
16
|
+
rescue
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
module Subscribers
|
|
3
|
+
class ActiveJobSubscriber
|
|
4
|
+
def self.attach
|
|
5
|
+
return if @attached
|
|
6
|
+
@attached = true
|
|
7
|
+
|
|
8
|
+
safe_serialize = ->(data) { JSON.parse(data.to_json) rescue data.to_s }
|
|
9
|
+
|
|
10
|
+
ActiveSupport::Notifications.subscribe("enqueue.active_job") do |*_args, payload|
|
|
11
|
+
job = payload[:job]
|
|
12
|
+
next if job.class.include?(Sidekiq::Worker)
|
|
13
|
+
|
|
14
|
+
if RailsTraceViewer::TraceContext.active?
|
|
15
|
+
trace_id = RailsTraceViewer::TraceContext.trace_id
|
|
16
|
+
parent_id = RailsTraceViewer::TraceContext.parent_id
|
|
17
|
+
else
|
|
18
|
+
trace_id = SecureRandom.uuid
|
|
19
|
+
parent_id = nil
|
|
20
|
+
RailsTraceViewer::TraceContext.start!(trace_id)
|
|
21
|
+
RailsTraceViewer::Collector.start_trace(trace_id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
enqueue_node_id = SecureRandom.uuid
|
|
25
|
+
|
|
26
|
+
node = {
|
|
27
|
+
id: enqueue_node_id,
|
|
28
|
+
parent_id: parent_id,
|
|
29
|
+
type: "job_enqueue",
|
|
30
|
+
name: job.class.name,
|
|
31
|
+
source: "ActiveJob Enqueue",
|
|
32
|
+
queue: job.queue_name,
|
|
33
|
+
job_id: job.job_id,
|
|
34
|
+
arguments: safe_serialize.call(job.arguments),
|
|
35
|
+
scheduled_at: job.scheduled_at,
|
|
36
|
+
priority: job.priority,
|
|
37
|
+
children: []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
RailsTraceViewer::JobLinkRegistry.register(job, trace_id: trace_id, enqueue_node_id: enqueue_node_id)
|
|
41
|
+
RailsTraceViewer::Collector.add_node(trace_id, node)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
ActiveSupport::Notifications.subscribe("perform_start.active_job") do |*_args, payload|
|
|
45
|
+
job = payload[:job]
|
|
46
|
+
|
|
47
|
+
RailsTraceViewer::JobLinkRegistry.on_perform(job) do |trace_id, enqueue_node_id|
|
|
48
|
+
RailsTraceViewer::TraceContext.start_job!(trace_id)
|
|
49
|
+
RailsTraceViewer::Collector.start_trace(trace_id)
|
|
50
|
+
|
|
51
|
+
node_id = SecureRandom.uuid
|
|
52
|
+
node = {
|
|
53
|
+
id: node_id,
|
|
54
|
+
parent_id: enqueue_node_id,
|
|
55
|
+
type: "job_perform",
|
|
56
|
+
name: job.class.name,
|
|
57
|
+
source: "ActiveJob Perform",
|
|
58
|
+
job_id: job.job_id,
|
|
59
|
+
arguments: safe_serialize.call(job.arguments),
|
|
60
|
+
executions: job.executions,
|
|
61
|
+
children: []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
RailsTraceViewer::Collector.add_node(trace_id, node)
|
|
65
|
+
RailsTraceViewer::TraceContext.push(node_id)
|
|
66
|
+
RailsTraceViewer::JobLinkRegistry.delete(job)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
module Subscribers
|
|
3
|
+
class ControllerSubscriber
|
|
4
|
+
def self.attach
|
|
5
|
+
return if @attached
|
|
6
|
+
@attached = true
|
|
7
|
+
|
|
8
|
+
ActiveSupport::Notifications.subscribe("start_processing.action_controller") do |*, payload|
|
|
9
|
+
controller_name = payload[:controller].to_s
|
|
10
|
+
next if controller_name.start_with?("RailsTraceViewer::")
|
|
11
|
+
|
|
12
|
+
trace_id = SecureRandom.uuid
|
|
13
|
+
RailsTraceViewer::TraceContext.start!(trace_id)
|
|
14
|
+
RailsTraceViewer::Collector.start_trace(trace_id)
|
|
15
|
+
|
|
16
|
+
safe_params = payload[:params].to_unsafe_h rescue payload[:params]
|
|
17
|
+
safe_params = safe_params.except("controller", "action")
|
|
18
|
+
|
|
19
|
+
route = RailsTraceViewer::RouteResolver.resolve(payload[:path], payload[:method])
|
|
20
|
+
route_node_id = SecureRandom.uuid
|
|
21
|
+
|
|
22
|
+
route_node = {
|
|
23
|
+
id: route_node_id,
|
|
24
|
+
parent_id: nil,
|
|
25
|
+
type: "route",
|
|
26
|
+
name: "#{route[:verb]} #{route[:path]}",
|
|
27
|
+
source: payload[:path],
|
|
28
|
+
verb: route[:verb],
|
|
29
|
+
url_pattern: route[:path],
|
|
30
|
+
route_name: route[:name],
|
|
31
|
+
children: []
|
|
32
|
+
}
|
|
33
|
+
RailsTraceViewer::Collector.add_node(trace_id, route_node)
|
|
34
|
+
|
|
35
|
+
request_node = {
|
|
36
|
+
id: trace_id,
|
|
37
|
+
parent_id: route_node_id,
|
|
38
|
+
type: "request",
|
|
39
|
+
name: "#{payload[:controller]}##{payload[:action]}",
|
|
40
|
+
source: "#{payload[:controller]}.rb",
|
|
41
|
+
format: payload[:format],
|
|
42
|
+
params: safe_params,
|
|
43
|
+
children: []
|
|
44
|
+
}
|
|
45
|
+
RailsTraceViewer::Collector.add_node(trace_id, request_node)
|
|
46
|
+
RailsTraceViewer::TraceContext.push(trace_id)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
module Subscribers
|
|
3
|
+
class MethodSubscriber
|
|
4
|
+
@trace_point = nil
|
|
5
|
+
|
|
6
|
+
def self.attach
|
|
7
|
+
return if @trace_point
|
|
8
|
+
|
|
9
|
+
@trace_point = TracePoint.trace(:call, :return) do |tp|
|
|
10
|
+
path = tp.path.to_s
|
|
11
|
+
|
|
12
|
+
is_app_path = path.start_with?(Rails.root.to_s) &&
|
|
13
|
+
!path.include?("/vendor/") &&
|
|
14
|
+
!path.include?("rails_trace_viewer")
|
|
15
|
+
is_active_job = (tp.defined_class < ApplicationJob) rescue false
|
|
16
|
+
is_sidekiq_worker = (tp.defined_class.include?(Sidekiq::Worker)) rescue false
|
|
17
|
+
|
|
18
|
+
next unless is_app_path || is_active_job || is_sidekiq_worker
|
|
19
|
+
|
|
20
|
+
unless RailsTraceViewer::TraceContext.active?
|
|
21
|
+
trace_id = SecureRandom.uuid
|
|
22
|
+
RailsTraceViewer::TraceContext.start!(trace_id)
|
|
23
|
+
RailsTraceViewer::Collector.start_trace(trace_id)
|
|
24
|
+
parent_id = nil
|
|
25
|
+
else
|
|
26
|
+
trace_id = RailsTraceViewer::TraceContext.trace_id
|
|
27
|
+
parent_id = RailsTraceViewer::TraceContext.parent_id
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if tp.event == :call
|
|
31
|
+
node_id = SecureRandom.uuid
|
|
32
|
+
|
|
33
|
+
class_name = tp.defined_class.name rescue "Anonymous"
|
|
34
|
+
is_singleton = tp.defined_class.singleton_class? rescue false
|
|
35
|
+
method_sig = "#{class_name}#{is_singleton ? '.' : '#'}#{tp.method_id}"
|
|
36
|
+
|
|
37
|
+
node = {
|
|
38
|
+
id: node_id,
|
|
39
|
+
parent_id: parent_id,
|
|
40
|
+
type: "method",
|
|
41
|
+
name: method_sig,
|
|
42
|
+
source: "#{path.sub(Rails.root.to_s, '')}:#{tp.lineno}",
|
|
43
|
+
file_path: path,
|
|
44
|
+
line_number: tp.lineno,
|
|
45
|
+
class_name: class_name,
|
|
46
|
+
method_name: tp.method_id,
|
|
47
|
+
children: []
|
|
48
|
+
}
|
|
49
|
+
RailsTraceViewer::Collector.add_node(trace_id, node)
|
|
50
|
+
RailsTraceViewer::TraceContext.push(node_id)
|
|
51
|
+
|
|
52
|
+
elsif tp.event == :return
|
|
53
|
+
RailsTraceViewer::TraceContext.pop
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
module Subscribers
|
|
3
|
+
class SidekiqSubscriber
|
|
4
|
+
def self.attach
|
|
5
|
+
return unless defined?(Sidekiq)
|
|
6
|
+
return if @attached
|
|
7
|
+
@attached = true
|
|
8
|
+
|
|
9
|
+
Sidekiq.configure_server do |config|
|
|
10
|
+
config.server_middleware { |chain| chain.add ServerMiddleware }
|
|
11
|
+
config.client_middleware { |chain| chain.add ClientMiddleware }
|
|
12
|
+
end
|
|
13
|
+
Sidekiq.configure_client do |config|
|
|
14
|
+
config.client_middleware { |chain| chain.add ClientMiddleware }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module SafeSerializer
|
|
19
|
+
def self.serialize(args)
|
|
20
|
+
return args unless args.is_a?(Array) || args.is_a?(Hash)
|
|
21
|
+
JSON.parse(args.to_json) rescue ["(Unserializable)"]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ClientMiddleware
|
|
26
|
+
def call(worker_class, job, queue, redis_pool)
|
|
27
|
+
trace_id = nil
|
|
28
|
+
parent_id = nil
|
|
29
|
+
started_new_trace = false
|
|
30
|
+
|
|
31
|
+
if RailsTraceViewer::TraceContext.active?
|
|
32
|
+
trace_id = RailsTraceViewer::TraceContext.trace_id
|
|
33
|
+
parent_id = RailsTraceViewer::TraceContext.parent_id
|
|
34
|
+
else
|
|
35
|
+
trace_id = SecureRandom.uuid
|
|
36
|
+
RailsTraceViewer::TraceContext.start!(trace_id)
|
|
37
|
+
RailsTraceViewer::Collector.start_trace(trace_id)
|
|
38
|
+
started_new_trace = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
enqueue_node_id = SecureRandom.uuid
|
|
42
|
+
job["_trace_id"] = trace_id
|
|
43
|
+
job["_enqueue_node_id"] = enqueue_node_id
|
|
44
|
+
|
|
45
|
+
safe_args = SafeSerializer.serialize(job["args"])
|
|
46
|
+
|
|
47
|
+
node = {
|
|
48
|
+
id: enqueue_node_id,
|
|
49
|
+
parent_id: parent_id,
|
|
50
|
+
type: "job_enqueue",
|
|
51
|
+
name: "#{worker_class} (Enqueue)",
|
|
52
|
+
source: "Sidekiq Client",
|
|
53
|
+
worker_class: worker_class.to_s,
|
|
54
|
+
queue: queue,
|
|
55
|
+
job_arguments: safe_args,
|
|
56
|
+
jid: job["jid"],
|
|
57
|
+
children: []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
RailsTraceViewer::Collector.add_node(trace_id, node)
|
|
61
|
+
|
|
62
|
+
yield
|
|
63
|
+
ensure
|
|
64
|
+
if started_new_trace
|
|
65
|
+
RailsTraceViewer::TraceContext.stop!
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class ServerMiddleware
|
|
71
|
+
def call(worker, job, queue)
|
|
72
|
+
trace_id = job["_trace_id"]
|
|
73
|
+
parent_id = job["_enqueue_node_id"] || job["_parent_id"]
|
|
74
|
+
perform_node_id = nil
|
|
75
|
+
|
|
76
|
+
if trace_id
|
|
77
|
+
RailsTraceViewer::TraceContext.start_job!(trace_id)
|
|
78
|
+
RailsTraceViewer::Collector.start_trace(trace_id)
|
|
79
|
+
|
|
80
|
+
perform_node_id = SecureRandom.uuid
|
|
81
|
+
safe_args = SafeSerializer.serialize(job["args"])
|
|
82
|
+
|
|
83
|
+
node = {
|
|
84
|
+
id: perform_node_id,
|
|
85
|
+
parent_id: parent_id,
|
|
86
|
+
type: "job_perform",
|
|
87
|
+
name: "#{worker.class} (Perform)",
|
|
88
|
+
source: "Sidekiq Server",
|
|
89
|
+
worker_class: worker.class.to_s,
|
|
90
|
+
queue: queue,
|
|
91
|
+
job_arguments: safe_args,
|
|
92
|
+
jid: job["jid"],
|
|
93
|
+
retry_count: job["retry_count"],
|
|
94
|
+
children: []
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
RailsTraceViewer::Collector.add_node(trace_id, node)
|
|
98
|
+
RailsTraceViewer::TraceContext.push(perform_node_id)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
yield
|
|
102
|
+
ensure
|
|
103
|
+
if trace_id
|
|
104
|
+
RailsTraceViewer::TraceContext.pop if perform_node_id
|
|
105
|
+
RailsTraceViewer::TraceContext.stop!
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
module Subscribers
|
|
3
|
+
class SqlSubscriber
|
|
4
|
+
def self.attach
|
|
5
|
+
return if @attached
|
|
6
|
+
@attached = true
|
|
7
|
+
|
|
8
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |*_args, payload|
|
|
9
|
+
next unless RailsTraceViewer::TraceContext.active?
|
|
10
|
+
next if payload[:name] == "SCHEMA"
|
|
11
|
+
|
|
12
|
+
source = extract_app_source(caller)
|
|
13
|
+
next unless source
|
|
14
|
+
|
|
15
|
+
raw_binds = payload[:type_casted_binds]
|
|
16
|
+
if raw_binds.respond_to?(:call)
|
|
17
|
+
raw_binds = raw_binds.call
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
binds = (raw_binds || []).map { |val| val.to_s }
|
|
21
|
+
|
|
22
|
+
node = {
|
|
23
|
+
id: SecureRandom.uuid,
|
|
24
|
+
parent_id: RailsTraceViewer::TraceContext.parent_id,
|
|
25
|
+
type: "sql",
|
|
26
|
+
name: payload[:sql],
|
|
27
|
+
source: source,
|
|
28
|
+
full_sql: payload[:sql],
|
|
29
|
+
bind_values: binds,
|
|
30
|
+
connection_id: payload[:connection_id],
|
|
31
|
+
children: []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
RailsTraceViewer::Collector.add_node(RailsTraceViewer::TraceContext.trace_id, node)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.extract_app_source(bt)
|
|
39
|
+
bt.find { |line| line.include?("/app/") }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
module Subscribers
|
|
3
|
+
class ViewSubscriber
|
|
4
|
+
EVENTS = ["render_template.action_view", "render_partial.action_view"]
|
|
5
|
+
|
|
6
|
+
def self.attach
|
|
7
|
+
return if @attached
|
|
8
|
+
@attached = true
|
|
9
|
+
|
|
10
|
+
EVENTS.each do |event|
|
|
11
|
+
ActiveSupport::Notifications.subscribe(event) do |*_args, payload|
|
|
12
|
+
next unless RailsTraceViewer::TraceContext.active?
|
|
13
|
+
|
|
14
|
+
file = payload[:identifier]
|
|
15
|
+
next unless file && file.include?(Rails.root.join("app").to_s)
|
|
16
|
+
|
|
17
|
+
node = {
|
|
18
|
+
id: SecureRandom.uuid,
|
|
19
|
+
parent_id: RailsTraceViewer::TraceContext.parent_id,
|
|
20
|
+
type: "view",
|
|
21
|
+
name: file.split("/app/").last,
|
|
22
|
+
source: file.sub(Rails.root.to_s, ''),
|
|
23
|
+
layout: payload[:layout],
|
|
24
|
+
full_path: file,
|
|
25
|
+
children: []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
RailsTraceViewer::Collector.add_node(RailsTraceViewer::TraceContext.trace_id, node)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module RailsTraceViewer
|
|
2
|
+
class TraceContext
|
|
3
|
+
THREAD_KEY = :rails_trace_viewer_context
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def context
|
|
7
|
+
Thread.current[THREAD_KEY] ||= {
|
|
8
|
+
active: false,
|
|
9
|
+
trace_id: nil,
|
|
10
|
+
stack: []
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start!(trace_id)
|
|
15
|
+
ctx = context
|
|
16
|
+
ctx[:active] = true
|
|
17
|
+
ctx[:trace_id] = trace_id
|
|
18
|
+
ctx[:stack] = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start_job!(trace_id)
|
|
22
|
+
ctx = context
|
|
23
|
+
ctx[:active] = true
|
|
24
|
+
ctx[:trace_id] = trace_id
|
|
25
|
+
ctx[:stack] ||= []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def stop!
|
|
29
|
+
ctx = context
|
|
30
|
+
ctx[:active] = false
|
|
31
|
+
ctx[:trace_id] = nil
|
|
32
|
+
ctx[:stack] = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def active?
|
|
36
|
+
context[:active]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def trace_id
|
|
40
|
+
context[:trace_id]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parent_id
|
|
44
|
+
context[:stack].last
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def push(node_id)
|
|
48
|
+
context[:stack] << node_id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pop
|
|
52
|
+
context[:stack].pop
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|