vivarium 0.2.0 → 0.3.1
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/CONTEXT.md +535 -0
- data/README.md +16 -12
- data/examples/execve_demo.rb +4 -1
- data/examples/file_operation_demo.rb +4 -1
- data/examples/network_client_demo.rb +5 -1
- data/examples/privilege_event_demo.rb +5 -1
- data/examples/raise_demo.rb +46 -0
- data/examples/signal_kill_demo.rb +5 -1
- data/examples/ssl_write_demo.rb +37 -0
- data/examples/sudo_attempt_demo.rb +18 -0
- data/exe/vivarium +6 -0
- data/image.png +0 -0
- data/lib/vivarium/cli.rb +40 -0
- data/lib/vivarium/correlator.rb +139 -0
- data/lib/vivarium/display_filter.rb +158 -0
- data/lib/vivarium/http_decoder.rb +237 -0
- data/lib/vivarium/tree_renderer.rb +593 -0
- data/lib/vivarium/version.rb +1 -1
- data/lib/vivarium.rb +446 -175
- data/logo-simple.png +0 -0
- metadata +31 -5
- data/lib/vivarium/logger.rb +0 -80
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
require_relative "http_decoder"
|
|
5
|
+
|
|
6
|
+
module Vivarium
|
|
7
|
+
class TreeRenderer
|
|
8
|
+
SPAN_EVENT_NAMES = %w[span_start span_stop].to_set.freeze
|
|
9
|
+
FORK_EVENT_NAME = "proc_fork"
|
|
10
|
+
EXEC_EVENT_NAME = "proc_exec"
|
|
11
|
+
SSL_WRITE_EVENT_NAME = "ssl_write"
|
|
12
|
+
|
|
13
|
+
LSM_EVENT_NAMES = %w[
|
|
14
|
+
path_open sock_connect odd_socket
|
|
15
|
+
ptrace_check sb_mount kernel_read_file task_kill
|
|
16
|
+
setid_change capable_check bprm_creds
|
|
17
|
+
file_symlink file_hardlink file_rename file_chmod
|
|
18
|
+
].to_set.freeze
|
|
19
|
+
|
|
20
|
+
TP_EVENT_NAMES = %w[
|
|
21
|
+
dns_req proc_exec file_getdents proc_fork
|
|
22
|
+
].to_set.freeze
|
|
23
|
+
|
|
24
|
+
UPROBE_EVENT_NAMES = %w[ssl_write].to_set.freeze
|
|
25
|
+
|
|
26
|
+
SYNTHETIC_SPAN_NAME = "<no-span>"
|
|
27
|
+
UNRESOLVED_METHOD_PREFIX = "<method_id="
|
|
28
|
+
|
|
29
|
+
Span = Struct.new(
|
|
30
|
+
:tid, :method_id, :file_id, :lineno, :start_ktime, :stop_ktime, :exit_kind,
|
|
31
|
+
:events, :descendant_pids, :synthetic, :raised,
|
|
32
|
+
keyword_init: true
|
|
33
|
+
) do
|
|
34
|
+
def duration_ns
|
|
35
|
+
return nil unless stop_ktime && start_ktime
|
|
36
|
+
|
|
37
|
+
stop_ktime - start_ktime
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Span is mutable (stop_ktime, events, descendant_pids are written after creation).
|
|
41
|
+
# Use object identity for Hash/Set so keys remain stable across mutations.
|
|
42
|
+
def hash
|
|
43
|
+
object_id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def eql?(other)
|
|
47
|
+
equal?(other)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
EventNode = Struct.new(:kind, :name, :target, :offset_ns, :child_proc, keyword_init: true)
|
|
52
|
+
ProcNode = Struct.new(:pid, :comm, :parent_pid, :children, keyword_init: true)
|
|
53
|
+
|
|
54
|
+
def initialize(events:, method_table:, observer_pid:, main_tid:,
|
|
55
|
+
session_start_iso:, session_start_ktime:,
|
|
56
|
+
session_stop_iso:, session_stop_ktime:, filter: nil, dest:)
|
|
57
|
+
@events = events
|
|
58
|
+
@method_table = method_table
|
|
59
|
+
@observer_pid = observer_pid
|
|
60
|
+
@main_tid = main_tid
|
|
61
|
+
@session_start_iso = session_start_iso
|
|
62
|
+
@session_start_ktime = session_start_ktime
|
|
63
|
+
@session_stop_iso = session_stop_iso
|
|
64
|
+
@session_stop_ktime = session_stop_ktime
|
|
65
|
+
@display_filter = Vivarium::DisplayFilter.compile(filter)
|
|
66
|
+
@dest = dest
|
|
67
|
+
|
|
68
|
+
@pid_comm = { observer_pid => "ruby" }
|
|
69
|
+
@pid_parent = {}
|
|
70
|
+
@unresolved_method_ids = []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def render
|
|
74
|
+
sorted = @events.sort_by { |e| [e.ktime_ns, e.pid, e.tid] }
|
|
75
|
+
|
|
76
|
+
real_spans, @children_map = build_real_spans(sorted)
|
|
77
|
+
@child_span_set = @children_map.values.flatten.to_set
|
|
78
|
+
|
|
79
|
+
assign_descendants(real_spans, sorted)
|
|
80
|
+
|
|
81
|
+
root_real_spans = real_spans.reject { |s| @child_span_set.include?(s) }
|
|
82
|
+
root_with_synthetics = interleave_synthetic_spans(root_real_spans)
|
|
83
|
+
|
|
84
|
+
synthetic_spans = root_with_synthetics.select(&:synthetic)
|
|
85
|
+
all_spans_for_assign = (synthetic_spans + real_spans).sort_by { |s| s.start_ktime || 0 }
|
|
86
|
+
assign_events_to_spans(all_spans_for_assign, sorted)
|
|
87
|
+
|
|
88
|
+
print_header
|
|
89
|
+
print_warnings
|
|
90
|
+
print_observer_proc(root_with_synthetics)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def build_real_spans(events)
|
|
96
|
+
open_by_tid = Hash.new { |h, k| h[k] = [] }
|
|
97
|
+
closed = []
|
|
98
|
+
children_map = {}
|
|
99
|
+
|
|
100
|
+
events.each do |ev|
|
|
101
|
+
case ev.event_name
|
|
102
|
+
when "span_start"
|
|
103
|
+
mid, fid, lno = read_span_payload(ev.payload)
|
|
104
|
+
span = Span.new(
|
|
105
|
+
tid: ev.tid,
|
|
106
|
+
method_id: mid,
|
|
107
|
+
file_id: fid,
|
|
108
|
+
lineno: lno,
|
|
109
|
+
start_ktime: ev.ktime_ns,
|
|
110
|
+
stop_ktime: nil,
|
|
111
|
+
exit_kind: nil,
|
|
112
|
+
events: [],
|
|
113
|
+
descendant_pids: Set.new,
|
|
114
|
+
synthetic: false,
|
|
115
|
+
raised: false
|
|
116
|
+
)
|
|
117
|
+
parent = open_by_tid[ev.tid].last
|
|
118
|
+
(children_map[parent] ||= []) << span if parent
|
|
119
|
+
open_by_tid[ev.tid].push(span)
|
|
120
|
+
when "span_stop"
|
|
121
|
+
stack = open_by_tid[ev.tid]
|
|
122
|
+
next if stack.empty?
|
|
123
|
+
|
|
124
|
+
span = stack.pop
|
|
125
|
+
span.stop_ktime = ev.ktime_ns
|
|
126
|
+
span.exit_kind = :stopped
|
|
127
|
+
closed << span
|
|
128
|
+
when "span_raise"
|
|
129
|
+
span = open_by_tid[ev.tid].last
|
|
130
|
+
span.raised = true if span
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
open_by_tid.each_value do |stack|
|
|
135
|
+
stack.each do |span|
|
|
136
|
+
span.stop_ktime = @session_stop_ktime || (events.last&.ktime_ns)
|
|
137
|
+
span.exit_kind = :dangling
|
|
138
|
+
closed << span
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
closed.sort_by!(&:start_ktime)
|
|
143
|
+
[closed, children_map]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def assign_descendants(spans, events)
|
|
147
|
+
sorted_spans = spans.reject(&:synthetic).sort_by(&:start_ktime)
|
|
148
|
+
|
|
149
|
+
events.each do |ev|
|
|
150
|
+
next unless ev.event_name == FORK_EVENT_NAME
|
|
151
|
+
|
|
152
|
+
child_pid = read_proc_fork_child_pid(ev.payload)
|
|
153
|
+
next if child_pid.zero?
|
|
154
|
+
|
|
155
|
+
@pid_parent[child_pid] = ev.pid
|
|
156
|
+
|
|
157
|
+
owning = innermost_span_for_event(sorted_spans, ev)
|
|
158
|
+
owning&.descendant_pids&.add(child_pid)
|
|
159
|
+
|
|
160
|
+
# Closure: if ev.pid is itself a descendant of some span, that span also gains child_pid
|
|
161
|
+
sorted_spans.each do |span|
|
|
162
|
+
next if span == owning
|
|
163
|
+
next unless event_in_span?(ev, span)
|
|
164
|
+
|
|
165
|
+
span.descendant_pids.add(child_pid) if span.descendant_pids.include?(ev.pid)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def interleave_synthetic_spans(real_spans)
|
|
171
|
+
result = []
|
|
172
|
+
cursor = @session_start_ktime || (real_spans.first&.start_ktime) || 0
|
|
173
|
+
session_end = @session_stop_ktime ||
|
|
174
|
+
real_spans.map(&:stop_ktime).compact.max ||
|
|
175
|
+
cursor
|
|
176
|
+
|
|
177
|
+
real_spans.each do |span|
|
|
178
|
+
if span.start_ktime > cursor
|
|
179
|
+
syn = synthetic_span(cursor, span.start_ktime)
|
|
180
|
+
result << syn if syn
|
|
181
|
+
end
|
|
182
|
+
result << span
|
|
183
|
+
cursor = [cursor, span.stop_ktime || span.start_ktime].max
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if session_end > cursor
|
|
187
|
+
syn = synthetic_span(cursor, session_end)
|
|
188
|
+
result << syn if syn
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
result
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def synthetic_span(start_ktime, stop_ktime)
|
|
195
|
+
Span.new(
|
|
196
|
+
tid: @main_tid,
|
|
197
|
+
method_id: nil,
|
|
198
|
+
file_id: nil,
|
|
199
|
+
lineno: nil,
|
|
200
|
+
start_ktime: start_ktime,
|
|
201
|
+
stop_ktime: stop_ktime,
|
|
202
|
+
exit_kind: :stopped,
|
|
203
|
+
events: [],
|
|
204
|
+
descendant_pids: Set.new,
|
|
205
|
+
synthetic: true,
|
|
206
|
+
raised: false
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def assign_events_to_spans(spans, events)
|
|
211
|
+
events.each do |ev|
|
|
212
|
+
next if SPAN_EVENT_NAMES.include?(ev.event_name)
|
|
213
|
+
|
|
214
|
+
if ev.event_name == "proc_exec"
|
|
215
|
+
@pid_comm[ev.pid] = exec_basename(ev.payload) || @pid_comm[ev.pid] || "?"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
host = find_host_span(spans, ev)
|
|
219
|
+
next unless host
|
|
220
|
+
|
|
221
|
+
host.events << ev
|
|
222
|
+
if ev.event_name == FORK_EVENT_NAME
|
|
223
|
+
child_pid = read_proc_fork_child_pid(ev.payload)
|
|
224
|
+
@pid_comm[child_pid] ||= "?"
|
|
225
|
+
host.descendant_pids.add(child_pid)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def find_host_span(spans, ev)
|
|
231
|
+
candidates = spans.select { |s| event_in_span?(ev, s) }
|
|
232
|
+
return nil if candidates.empty?
|
|
233
|
+
|
|
234
|
+
direct = candidates.select { |s| s.tid == ev.tid }
|
|
235
|
+
return direct.last unless direct.empty?
|
|
236
|
+
|
|
237
|
+
desc = candidates.select { |s| s.descendant_pids.include?(ev.pid) }
|
|
238
|
+
return desc.last unless desc.empty?
|
|
239
|
+
|
|
240
|
+
candidates.find(&:synthetic) || candidates.last
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def innermost_span_for_event(spans, ev)
|
|
244
|
+
candidates = spans.select { |s| event_in_span?(ev, s) && s.tid == ev.tid }
|
|
245
|
+
candidates.last
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def event_in_span?(ev, span)
|
|
249
|
+
return false unless span.start_ktime
|
|
250
|
+
return false if span.stop_ktime && ev.ktime_ns > span.stop_ktime
|
|
251
|
+
|
|
252
|
+
ev.ktime_ns >= span.start_ktime
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def print_header
|
|
256
|
+
duration_s = ((@session_stop_ktime || 0) - (@session_start_ktime || 0)) / 1_000_000_000.0
|
|
257
|
+
@dest.puts "# vivarium session"
|
|
258
|
+
@dest.puts "# started iso=#{@session_start_iso} ktime=#{@session_start_ktime}"
|
|
259
|
+
@dest.puts "# stopped iso=#{@session_stop_iso} ktime=#{@session_stop_ktime}"
|
|
260
|
+
@dest.puts "# duration #{format('%.3fs', duration_s)}"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def print_warnings
|
|
264
|
+
@unresolved_method_ids.uniq.each do |mid|
|
|
265
|
+
@dest.puts format("# warning method_id=0x%016X unresolved at render time", mid & 0xFFFF_FFFF_FFFF_FFFF)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def print_observer_proc(spans)
|
|
270
|
+
@dest.puts "[PROC pid=#{@observer_pid} comm=#{@pid_comm[@observer_pid] || 'ruby'}]"
|
|
271
|
+
children = spans.reject { |s| s.synthetic && s.events.empty? }
|
|
272
|
+
.reject { |s| @child_span_set.include?(s) }
|
|
273
|
+
.select { |s| span_visible?(s) }
|
|
274
|
+
children.each_with_index do |span, idx|
|
|
275
|
+
print_span(span, prefix: "", is_last: idx == children.size - 1)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def print_span(span, prefix:, is_last:)
|
|
280
|
+
marker = is_last ? "└─ " : "├─ "
|
|
281
|
+
@dest.puts "#{prefix}#{marker}#{render_span_header(span)}"
|
|
282
|
+
child_prefix = prefix + (is_last ? " " : "│ ")
|
|
283
|
+
nodes = build_span_children(span)
|
|
284
|
+
print_nodes(nodes, child_prefix)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def render_span_header(span)
|
|
288
|
+
name = span_display_name(span)
|
|
289
|
+
dur_text = format_duration(span.duration_ns)
|
|
290
|
+
file_info = span_file_info(span)
|
|
291
|
+
suffix = if span.exit_kind == :dangling
|
|
292
|
+
" (open)"
|
|
293
|
+
elsif span.raised
|
|
294
|
+
" (raise)"
|
|
295
|
+
else
|
|
296
|
+
""
|
|
297
|
+
end
|
|
298
|
+
"[SPAN tid=#{span.tid} #{name}#{file_info} dur=#{dur_text}#{suffix}]"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def span_file_info(span)
|
|
302
|
+
return "" if span.synthetic
|
|
303
|
+
return "" unless span.file_id && span.file_id != -1
|
|
304
|
+
|
|
305
|
+
file_name = Vivarium::Usdt.get_file_name(span.file_id)
|
|
306
|
+
return "" unless file_name
|
|
307
|
+
|
|
308
|
+
lno = span.lineno && span.lineno > 0 ? ":#{span.lineno}" : ""
|
|
309
|
+
" at=#{File.basename(file_name)}#{lno}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def span_display_name(span)
|
|
313
|
+
return SYNTHETIC_SPAN_NAME if span.synthetic
|
|
314
|
+
return SYNTHETIC_SPAN_NAME if span.method_id.nil?
|
|
315
|
+
|
|
316
|
+
name = @method_table[span.method_id]
|
|
317
|
+
name ||= Vivarium::Usdt.get_method_name(span.method_id)
|
|
318
|
+
return name if name
|
|
319
|
+
|
|
320
|
+
@unresolved_method_ids << span.method_id
|
|
321
|
+
format("#{UNRESOLVED_METHOD_PREFIX}0x%016X>", span.method_id & 0xFFFF_FFFF_FFFF_FFFF)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def build_span_children(span)
|
|
325
|
+
proc_node_by_pid = {}
|
|
326
|
+
root_children = []
|
|
327
|
+
|
|
328
|
+
span.events.each do |ev|
|
|
329
|
+
target_text = nil
|
|
330
|
+
if @display_filter.needs_payload?
|
|
331
|
+
target_text = render_target(ev)
|
|
332
|
+
next unless event_visible?(ev, span, target_text)
|
|
333
|
+
else
|
|
334
|
+
next unless event_visible?(ev, span)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
if ev.event_name == FORK_EVENT_NAME
|
|
338
|
+
child_pid = read_proc_fork_child_pid(ev.payload)
|
|
339
|
+
child_node = ProcNode.new(
|
|
340
|
+
pid: child_pid,
|
|
341
|
+
comm: @pid_comm[child_pid] || "?",
|
|
342
|
+
parent_pid: ev.pid,
|
|
343
|
+
children: []
|
|
344
|
+
)
|
|
345
|
+
proc_node_by_pid[child_pid] = child_node
|
|
346
|
+
|
|
347
|
+
ev_node = EventNode.new(
|
|
348
|
+
kind: kind_for(ev),
|
|
349
|
+
name: ev.event_name,
|
|
350
|
+
target: target_text || render_target(ev),
|
|
351
|
+
offset_ns: ev.ktime_ns - span.start_ktime,
|
|
352
|
+
child_proc: child_node
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
parent_container = container_for_pid(ev.pid, span, proc_node_by_pid, root_children)
|
|
356
|
+
append_event(parent_container, ev_node)
|
|
357
|
+
else
|
|
358
|
+
ev_node = EventNode.new(
|
|
359
|
+
kind: kind_for(ev),
|
|
360
|
+
name: ev.event_name,
|
|
361
|
+
target: target_text || render_target(ev),
|
|
362
|
+
offset_ns: ev.ktime_ns - span.start_ktime,
|
|
363
|
+
child_proc: nil
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
if ev.pid == @observer_pid && ev.tid == span.tid
|
|
367
|
+
append_event(root_children, ev_node)
|
|
368
|
+
elsif (node = proc_node_by_pid[ev.pid])
|
|
369
|
+
append_event(node.children, ev_node)
|
|
370
|
+
else
|
|
371
|
+
# event from a descendant pid we haven't materialized — synthesize a stub PROC node
|
|
372
|
+
stub = ProcNode.new(
|
|
373
|
+
pid: ev.pid,
|
|
374
|
+
comm: @pid_comm[ev.pid] || "?",
|
|
375
|
+
parent_pid: @pid_parent[ev.pid] || span.tid,
|
|
376
|
+
children: []
|
|
377
|
+
)
|
|
378
|
+
proc_node_by_pid[ev.pid] = stub
|
|
379
|
+
append_event(stub.children, ev_node)
|
|
380
|
+
root_children << stub
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if ev.event_name == EXEC_EVENT_NAME && (node = proc_node_by_pid[ev.pid])
|
|
384
|
+
node.comm = @pid_comm[ev.pid] || node.comm
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Interleave child spans by start time among the event/proc nodes
|
|
390
|
+
child_spans = (@children_map[span] || []).sort_by(&:start_ktime)
|
|
391
|
+
child_spans.each do |child_span|
|
|
392
|
+
child_offset = child_span.start_ktime - span.start_ktime
|
|
393
|
+
insert_pos = root_children.size
|
|
394
|
+
root_children.each_with_index do |node, i|
|
|
395
|
+
if node.is_a?(EventNode) && node.offset_ns >= child_offset
|
|
396
|
+
insert_pos = i
|
|
397
|
+
break
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
root_children.insert(insert_pos, child_span)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
root_children
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def container_for_pid(pid, span, proc_node_by_pid, root_children)
|
|
407
|
+
return root_children if pid == @observer_pid
|
|
408
|
+
|
|
409
|
+
node = proc_node_by_pid[pid]
|
|
410
|
+
return node.children if node
|
|
411
|
+
|
|
412
|
+
# Walk up parent chain to find a known node
|
|
413
|
+
cur = pid
|
|
414
|
+
while (parent = @pid_parent[cur])
|
|
415
|
+
return root_children if parent == @observer_pid
|
|
416
|
+
if (parent_node = proc_node_by_pid[parent])
|
|
417
|
+
stub = ProcNode.new(pid: pid, comm: @pid_comm[pid] || "?", parent_pid: parent, children: [])
|
|
418
|
+
proc_node_by_pid[pid] = stub
|
|
419
|
+
parent_node.children << stub
|
|
420
|
+
return stub.children
|
|
421
|
+
end
|
|
422
|
+
cur = parent
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
root_children
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def append_event(container, ev_node)
|
|
429
|
+
container << ev_node
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def print_nodes(nodes, prefix)
|
|
433
|
+
visible_nodes = nodes.select do |node|
|
|
434
|
+
!node.is_a?(Span) || span_visible?(node)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
visible_nodes.each_with_index do |node, idx|
|
|
438
|
+
is_last = idx == visible_nodes.size - 1
|
|
439
|
+
case node
|
|
440
|
+
when EventNode
|
|
441
|
+
print_event_node(node, prefix: prefix, is_last: is_last)
|
|
442
|
+
when ProcNode
|
|
443
|
+
print_proc_node(node, prefix: prefix, is_last: is_last)
|
|
444
|
+
when Span
|
|
445
|
+
print_span(node, prefix: prefix, is_last: is_last)
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def span_visible?(span)
|
|
451
|
+
@display_filter.allow_span_name?(span_display_name(span))
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def event_visible?(ev, span, target_text = nil)
|
|
455
|
+
@display_filter.allow_event?(
|
|
456
|
+
event_name: ev.event_name,
|
|
457
|
+
severity: Vivarium.event_severity(ev.event_name),
|
|
458
|
+
span_name: span_display_name(span),
|
|
459
|
+
payload: target_text,
|
|
460
|
+
pid: ev.pid,
|
|
461
|
+
tid: ev.tid
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def print_event_node(node, prefix:, is_last:)
|
|
466
|
+
marker = is_last && node.child_proc.nil? ? "└─ " : "├─ "
|
|
467
|
+
marker = "└─ " if is_last && node.child_proc.nil?
|
|
468
|
+
marker = "├─ " unless is_last
|
|
469
|
+
marker = "└─ " if is_last
|
|
470
|
+
offset_text = format_offset(node.offset_ns)
|
|
471
|
+
line = format("%-4s %-15s → %-30s @+%s", node.kind, node.name, node.target, offset_text)
|
|
472
|
+
@dest.puts "#{prefix}#{marker}#{line}"
|
|
473
|
+
|
|
474
|
+
if node.child_proc
|
|
475
|
+
child_prefix = prefix + (is_last ? " " : "│ ")
|
|
476
|
+
print_proc_node(node.child_proc, prefix: child_prefix, is_last: true)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def print_proc_node(node, prefix:, is_last:)
|
|
481
|
+
marker = is_last ? "└─ " : "├─ "
|
|
482
|
+
header = "[PROC pid=#{node.pid} comm=#{node.comm}"
|
|
483
|
+
header += " parent=#{node.parent_pid}" if node.parent_pid
|
|
484
|
+
header += "]"
|
|
485
|
+
@dest.puts "#{prefix}#{marker}#{header}"
|
|
486
|
+
child_prefix = prefix + (is_last ? " " : "│ ")
|
|
487
|
+
print_nodes(node.children, child_prefix)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def kind_for(ev)
|
|
491
|
+
return "EXCP" if ev.event_name == "span_raise"
|
|
492
|
+
return "USDT" if SPAN_EVENT_NAMES.include?(ev.event_name)
|
|
493
|
+
return "SSL" if ev.event_name == SSL_WRITE_EVENT_NAME
|
|
494
|
+
return "LSM" if LSM_EVENT_NAMES.include?(ev.event_name)
|
|
495
|
+
return "TP" if TP_EVENT_NAMES.include?(ev.event_name)
|
|
496
|
+
|
|
497
|
+
"EVT"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def render_target(ev)
|
|
501
|
+
return render_raise_target(ev) if ev.event_name == "span_raise"
|
|
502
|
+
return render_ssl_write_target(ev) if ev.event_name == SSL_WRITE_EVENT_NAME
|
|
503
|
+
|
|
504
|
+
text = Vivarium.render_event_payload(ev).to_s
|
|
505
|
+
text = text.gsub(/\s+/, " ").strip
|
|
506
|
+
text.empty? ? "-" : text
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def render_ssl_write_target(ev)
|
|
510
|
+
decoded = Vivarium.decode_ssl_write_payload(ev.payload)
|
|
511
|
+
http_decoder.render(
|
|
512
|
+
pid: ev.pid,
|
|
513
|
+
data: decoded[:data],
|
|
514
|
+
data_len: decoded[:data_len]
|
|
515
|
+
)
|
|
516
|
+
rescue StandardError => e
|
|
517
|
+
"ssl_write <decode-error: #{e.class}: #{e.message}>"
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def http_decoder
|
|
521
|
+
@http_decoder ||= Vivarium::HttpDecoder.new
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def render_raise_target(ev)
|
|
525
|
+
bytes = ev.payload.to_s.b
|
|
526
|
+
return "-" if bytes.bytesize < 8
|
|
527
|
+
|
|
528
|
+
error_id = bytes[0, 8].unpack1("q<")
|
|
529
|
+
message_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
|
|
530
|
+
file_id = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
|
|
531
|
+
lineno = bytes.bytesize >= 32 ? bytes[24, 8].unpack1("q<") : -1
|
|
532
|
+
|
|
533
|
+
error_name = Vivarium::Usdt.get_error_name(error_id) ||
|
|
534
|
+
format("0x%016X", error_id & 0xFFFF_FFFF_FFFF_FFFF)
|
|
535
|
+
|
|
536
|
+
parts = ["error=#{error_name}"]
|
|
537
|
+
|
|
538
|
+
if message_id != -1
|
|
539
|
+
msg = Vivarium::Usdt.get_message_name(message_id)
|
|
540
|
+
parts << "message=#{msg.inspect}" if msg
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
if file_id != -1 && (file_name = Vivarium::Usdt.get_file_name(file_id))
|
|
544
|
+
lno = lineno && lineno > 0 ? ":#{lineno}" : ""
|
|
545
|
+
parts << "at=#{File.basename(file_name)}#{lno}"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
parts.join(" ")
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def format_duration(ns)
|
|
552
|
+
return "?" unless ns
|
|
553
|
+
|
|
554
|
+
ms = ns / 1_000_000.0
|
|
555
|
+
ms < 1.0 ? format("%.1fus", ns / 1_000.0) : format("%.1fms", ms)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def format_offset(ns)
|
|
559
|
+
return "?" unless ns
|
|
560
|
+
|
|
561
|
+
ms = ns / 1_000_000.0
|
|
562
|
+
ms.abs < 1.0 ? format("%.1fus", ns / 1_000.0) : format("%.1fms", ms)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def read_span_payload(payload)
|
|
566
|
+
bytes = payload.to_s.b
|
|
567
|
+
return [0, -1, -1] if bytes.bytesize < 8
|
|
568
|
+
|
|
569
|
+
method_id = bytes[0, 8].unpack1("q<")
|
|
570
|
+
file_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
|
|
571
|
+
lineno = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
|
|
572
|
+
[method_id, file_id, lineno]
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def read_proc_fork_child_pid(payload)
|
|
576
|
+
bytes = payload.to_s.b
|
|
577
|
+
return 0 if bytes.bytesize < 4
|
|
578
|
+
|
|
579
|
+
bytes[0, 4].unpack1("L<")
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def exec_basename(payload)
|
|
583
|
+
slot_size = Vivarium::PROC_EXEC_SLOT_SIZE
|
|
584
|
+
bytes = payload.to_s.b
|
|
585
|
+
return nil if bytes.empty?
|
|
586
|
+
|
|
587
|
+
filename = Vivarium.c_string(bytes[0, slot_size])
|
|
588
|
+
return nil if filename.empty?
|
|
589
|
+
|
|
590
|
+
File.basename(filename)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
end
|
data/lib/vivarium/version.rb
CHANGED