Module: Vivarium::CLI
- Defined in:
- lib/vivarium/cli.rb
Class Method Summary collapse
-
.compute_otel_rows(events) ⇒ Object
Resolve each event’s effective OTel span by replaying span_start/span_stop into a per-tid stack of method-call spans on top of the BPF-provided thread/process span.
-
.dump_otel(events, dest) ⇒ Object
Flat per-event dump of the OTel-oriented fields, sorted by ktime, with method-call span nesting resolved by compute_otel_rows.
-
.resolve_report_filter(options) ⇒ Object
Resolve the report display filter by precedence: –all > –filter JSON > –event NAMES > DEFAULT_FILTER.
- .run!(argv = ARGV) ⇒ Object
- .run_load!(argv, options) ⇒ Object
- .run_report!(argv, options) ⇒ Object
Class Method Details
.compute_otel_rows(events) ⇒ Object
Resolve each event’s effective OTel span by replaying span_start/span_stop into a per-tid stack of method-call spans on top of the BPF-provided thread/process span. Returns [event, span_id, parent_span_id, depth] per event. A method span’s id is hashed from (trace_id, tid, span-start ktime): non-zero, unique within the trace, and stable across re-runs. When no method span is active, the thread span and its parent_span_id form the base frame. The stack is per-tid so spawned children nest independently.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/vivarium/cli.rb', line 145 def self.compute_otel_rows(events) sorted = events.sort_by { |e| [e.ktime_ns, e.pid, e.tid] } stacks = Hash.new { |h, k| h[k] = [] } # tid => [method_span_id, ...] parent_of = {} # method_span_id => parent span_id sorted.map do |e| thread_span = e.span_id.to_i stack = stacks[e.tid] case e.event_name when "span_start" parent = stack.empty? ? thread_span : stack.last span = Vivarium.synth_span_id(e.trace_hi.to_i, e.trace_lo.to_i, e.tid, e.ktime_ns) parent_of[span] = parent stack.push(span) [e, span, parent, stack.size] when "span_stop" if stack.empty? [e, thread_span, e.parent_span_id.to_i, 0] else span = stack.pop [e, span, parent_of[span] || thread_span, stack.size + 1] end else if stack.empty? [e, thread_span, e.parent_span_id.to_i, 0] else span = stack.last [e, span, parent_of[span] || thread_span, stack.size] end end end end |
.dump_otel(events, dest) ⇒ Object
Flat per-event dump of the OTel-oriented fields, sorted by ktime, with method-call span nesting resolved by compute_otel_rows. An alternative to the tree view for inspecting trace_id/span_id propagation.
125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/vivarium/cli.rb', line 125 def self.dump_otel(events, dest) header = format("%-18s %-7s %-7s %-6s %-6s %-16s %-32s %-16s %-16s %-5s %s", "ktime_ns", "pid", "tid", "uid", "gid", "comm", "trace_id", "span_id", "parent_span", "depth", "event") dest.puts header compute_otel_rows(events).each do |e, span, parent, depth| trace = format("%016x%016x", e.trace_hi.to_i, e.trace_lo.to_i) dest.puts format("%-18d %-7d %-7d %-6d %-6d %-16s %-32s %016x %016x %-5d %s", e.ktime_ns, e.pid, e.tid, e.uid.to_i, e.gid.to_i, e.comm.to_s, trace, span, parent, depth, e.event_name) end end |
.resolve_report_filter(options) ⇒ Object
Resolve the report display filter by precedence:
--all > --filter JSON > --event NAMES > DEFAULT_FILTER
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/vivarium/cli.rb', line 181 def self.resolve_report_filter() return nil if [:show_all] if [:filter_json] begin return JSON.parse([:filter_json]) rescue JSON::ParserError => e abort "Invalid --filter JSON: #{e.}" end end names = [:event_names] return { include_events: names } if names && !names.empty? Vivarium::DEFAULT_FILTER end |
.run!(argv = ARGV) ⇒ Object
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/vivarium/cli.rb', line 8 def self.run!(argv = ARGV) = { socket_path: Vivarium.socket_path, dest: $stdout } parser = OptionParser.new do |opts| opts. = "Usage: vivarium [options] <command> [args]" opts.separator "" opts.separator "Commands:" opts.separator " load <script> Load and observe a Ruby script" opts.separator " report <raw-file> Render a saved raw event file" opts.separator "" opts.separator "Options:" opts.on("--socket PATH", "vivariumd Unix domain socket path") { |v| [:socket_path] = v } opts.on("-o", "--output PATH", "Log output file (default: stdout)") { |v| [:dest] = File.open(v, "a") } opts.on("--save-raw PATH", "load: save raw events to PATH instead of rendering") { |v| [:save_raw] = v } opts.on("--otel-out PATH", "load: write OTLP/JSON spans to PATH instead of rendering") { |v| [:otel_out] = v } opts.on("--otel-endpoint URL", "load: stream OTLP/JSON spans to an OTLP/HTTP collector (or OTEL_EXPORTER_OTLP_ENDPOINT)") do |v| [:otel_endpoint] = v end opts.on("-a", "--all", "report: show all events (ignore default filter)") { [:show_all] = true } opts.on("--filter JSON", "report: filter as a JSON object (overrides --event/default)") { |v| [:filter_json] = v } opts.on("-e", "--event NAMES", "report: comma-separated event names to include") do |v| [:event_names] = v.split(",").map(&:strip).reject(&:empty?) end opts.on("-d", "--max-span-depth N", Integer, "report: collapse method spans deeper than N (events kept)") do |v| [:max_span_depth] = v end opts.on("-u", "--dedup-values", "load/report: show repeated path_open/mmap_exec/dlopen/env_caccess values only once") do [:dedup_values] = true end opts.on("--dump-otel", "report: dump per-event otel fields (trace/span/uid/gid/comm) instead of the tree") do [:dump_otel] = true end end # order! stops at the first non-option (the subcommand), so parse once to # collect options before the command, then again to collect options placed # after it (e.g. `vivarium report --dedup-values FILE`). begin parser.order!(argv) command = argv.shift parser.order!(argv) if command rescue OptionParser::ParseError => e abort "#{e.}\n\n#{parser.help}" end case command when "load" run_load!(argv, ) when "report" run_report!(argv, ) else abort parser.help end end |
.run_load!(argv, options) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/vivarium/cli.rb', line 61 def self.run_load!(argv, ) script = argv.shift abort "Usage: vivarium load <script>" unless script abort "File not found: #{script}" unless File.exist?(script) filter = Vivarium::DEFAULT_FILTER filter = filter.merge(dedup_values: true) if [:dedup_values] endpoint = [:otel_endpoint] || ENV["OTEL_EXPORTER_OTLP_ENDPOINT"] otel_out = [:otel_out] if endpoint && otel_out warn "[vivarium] --otel-endpoint takes precedence; ignoring --otel-out" otel_out = nil end Vivarium.observe(socket_path: [:socket_path], dest: [:dest], filter: filter, save_raw: [:save_raw], otel_out: otel_out, otel_endpoint: endpoint) do Kernel.load(File.(script)) end end |
.run_report!(argv, options) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/vivarium/cli.rb', line 83 def self.run_report!(argv, ) raw = argv.shift abort "Usage: vivarium report <raw-file>" unless raw abort "File not found: #{raw}" unless File.exist?(raw) data = begin File.open(raw, "rb") { |io| Vivarium::RawStore.load(io) } rescue Vivarium::RawStore::FormatError => e abort "Invalid vivarium-raw file #{raw}: #{e.}" end = data[:meta] if [:dump_otel] dump_otel(data[:events], [:dest]) return end filter = resolve_report_filter() if [:max_span_depth] filter = (filter || {}).merge(max_span_depth: [:max_span_depth]) end if [:dedup_values] filter = (filter || {}).merge(dedup_values: true) end Vivarium::TreeRenderer.new( events: data[:events], observer_pid: [:observer_pid], main_tid: [:main_tid], session_start_iso: [:session_start_iso], session_start_ktime: [:session_start_ktime], session_stop_iso: [:session_stop_iso], session_stop_ktime: [:session_stop_ktime], filter: filter, dest: [:dest] ).render end |