Module: Vivarium::CLI

Defined in:
lib/vivarium/cli.rb

Class Method Summary collapse

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(options)
  return nil if options[:show_all]

  if options[:filter_json]
    begin
      return JSON.parse(options[:filter_json])
    rescue JSON::ParserError => e
      abort "Invalid --filter JSON: #{e.message}"
    end
  end

  names = options[: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)
  options = { socket_path: Vivarium.socket_path, dest: $stdout }
  parser = OptionParser.new do |opts|
    opts.banner = "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| options[:socket_path] = v }
    opts.on("-o", "--output PATH", "Log output file (default: stdout)") { |v| options[:dest] = File.open(v, "a") }
    opts.on("--save-raw PATH", "load: save raw events to PATH instead of rendering") { |v| options[:save_raw] = v }
    opts.on("--otel-out PATH", "load: write OTLP/JSON spans to PATH instead of rendering") { |v| options[: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|
      options[:otel_endpoint] = v
    end
    opts.on("-a", "--all", "report: show all events (ignore default filter)") { options[:show_all] = true }
    opts.on("--filter JSON", "report: filter as a JSON object (overrides --event/default)") { |v| options[:filter_json] = v }
    opts.on("-e", "--event NAMES", "report: comma-separated event names to include") do |v|
      options[: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|
      options[: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
      options[:dedup_values] = true
    end
    opts.on("--dump-otel", "report: dump per-event otel fields (trace/span/uid/gid/comm) instead of the tree") do
      options[: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.message}\n\n#{parser.help}"
  end

  case command
  when "load"
    run_load!(argv, options)
  when "report"
    run_report!(argv, options)
  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, options)
  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 options[:dedup_values]

  endpoint = options[:otel_endpoint] || ENV["OTEL_EXPORTER_OTLP_ENDPOINT"]
  otel_out = options[:otel_out]
  if endpoint && otel_out
    warn "[vivarium] --otel-endpoint takes precedence; ignoring --otel-out"
    otel_out = nil
  end

  Vivarium.observe(socket_path: options[:socket_path], dest: options[:dest],
                   filter: filter, save_raw: options[:save_raw],
                   otel_out: otel_out, otel_endpoint: endpoint) do
    Kernel.load(File.expand_path(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, options)
  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.message}"
    end
  meta = data[:meta]

  if options[:dump_otel]
    dump_otel(data[:events], options[:dest])
    return
  end

  filter = resolve_report_filter(options)
  if options[:max_span_depth]
    filter = (filter || {}).merge(max_span_depth: options[:max_span_depth])
  end
  if options[:dedup_values]
    filter = (filter || {}).merge(dedup_values: true)
  end

  Vivarium::TreeRenderer.new(
    events: data[:events],
    observer_pid: meta[:observer_pid],
    main_tid: meta[:main_tid],
    session_start_iso: meta[:session_start_iso],
    session_start_ktime: meta[:session_start_ktime],
    session_stop_iso: meta[:session_stop_iso],
    session_stop_ktime: meta[:session_stop_ktime],
    filter: filter,
    dest: options[:dest]
  ).render
end