vivarium 0.5.2 → 0.6.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.
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "fiddle"
5
+ require "securerandom"
4
6
  require "socket"
5
7
 
6
8
  module Vivarium
@@ -43,21 +45,44 @@ module Vivarium
43
45
 
44
46
  # Wraps the daemon's live BPF target maps so the API can (un)register PIDs.
45
47
  class Registry
46
- def initialize(config_root_targets, config_spawned_targets)
48
+ # struct otel_ctx_t { trace_id{u64 hi; u64 lo}; u64 span_id; u64 parent_span_id; }
49
+ OTEL_CTX_PACK_FMT = "Q<Q<Q<Q<"
50
+
51
+ def initialize(config_root_targets, config_spawned_targets, otel_ctx = nil)
47
52
  @config_root_targets = config_root_targets
48
53
  @config_spawned_targets = config_spawned_targets
54
+ @otel_ctx = otel_ctx
49
55
  end
50
56
 
57
+ # Registering a root target marks the start of a trace: issue a fresh
58
+ # 128-bit trace_id and a root span_id (no parent) into the otel_ctx map,
59
+ # keyed by the root pid (== the main thread's tid).
51
60
  def register(pid)
52
61
  @config_root_targets[pid] = 1
62
+ return unless @otel_ctx
63
+
64
+ hi = SecureRandom.random_number(1 << 64)
65
+ lo = SecureRandom.random_number(1 << 64)
66
+ span = SecureRandom.random_number(1 << 64)
67
+ write_otel_ctx(pid, hi, lo, span, 0)
53
68
  end
54
69
 
55
70
  def unregister(pid)
56
71
  @config_root_targets.delete(pid)
57
72
  @config_spawned_targets.clear
73
+ @otel_ctx&.clear
58
74
  rescue KeyError
59
75
  nil
60
76
  end
77
+
78
+ private
79
+
80
+ def write_otel_ctx(tid, trace_hi, trace_lo, span_id, parent_span_id)
81
+ size = @otel_ctx.leafsize
82
+ ptr = Fiddle::Pointer.malloc(size)
83
+ ptr[0, size] = [trace_hi, trace_lo, span_id, parent_span_id].pack(OTEL_CTX_PACK_FMT)
84
+ @otel_ctx[tid] = ptr
85
+ end
61
86
  end
62
87
 
63
88
  # Minimal HTTP/1.1 server over a Unix domain socket exposing the daemon control API.
data/lib/vivarium/cli.rb CHANGED
@@ -18,18 +18,36 @@ module Vivarium
18
18
  opts.on("--socket PATH", "vivariumd Unix domain socket path") { |v| options[:socket_path] = v }
19
19
  opts.on("-o", "--output PATH", "Log output file (default: stdout)") { |v| options[:dest] = File.open(v, "a") }
20
20
  opts.on("--save-raw PATH", "load: save raw events to PATH instead of rendering") { |v| options[:save_raw] = v }
21
- opts.on("--all", "report: show all events (ignore default filter)") { options[:show_all] = true }
21
+ opts.on("--otel-out PATH", "load: write OTLP/JSON spans to PATH instead of rendering") { |v| options[:otel_out] = v }
22
+ opts.on("--otel-endpoint URL", "load: stream OTLP/JSON spans to an OTLP/HTTP collector (or OTEL_EXPORTER_OTLP_ENDPOINT)") do |v|
23
+ options[:otel_endpoint] = v
24
+ end
25
+ opts.on("-a", "--all", "report: show all events (ignore default filter)") { options[:show_all] = true }
22
26
  opts.on("--filter JSON", "report: filter as a JSON object (overrides --event/default)") { |v| options[:filter_json] = v }
23
- opts.on("--event NAMES", "report: comma-separated event names to include") do |v|
27
+ opts.on("-e", "--event NAMES", "report: comma-separated event names to include") do |v|
24
28
  options[:event_names] = v.split(",").map(&:strip).reject(&:empty?)
25
29
  end
26
- opts.on("--max-span-depth N", Integer, "report: collapse method spans deeper than N (events kept)") do |v|
30
+ opts.on("-d", "--max-span-depth N", Integer, "report: collapse method spans deeper than N (events kept)") do |v|
27
31
  options[:max_span_depth] = v
28
32
  end
33
+ opts.on("-u", "--dedup-values", "load/report: show repeated path_open/mmap_exec/dlopen/env_caccess values only once") do
34
+ options[:dedup_values] = true
35
+ end
36
+ opts.on("--dump-otel", "report: dump per-event otel fields (trace/span/uid/gid/comm) instead of the tree") do
37
+ options[:dump_otel] = true
38
+ end
39
+ end
40
+ # order! stops at the first non-option (the subcommand), so parse once to
41
+ # collect options before the command, then again to collect options placed
42
+ # after it (e.g. `vivarium report --dedup-values FILE`).
43
+ begin
44
+ parser.order!(argv)
45
+ command = argv.shift
46
+ parser.order!(argv) if command
47
+ rescue OptionParser::ParseError => e
48
+ abort "#{e.message}\n\n#{parser.help}"
29
49
  end
30
- parser.order!(argv)
31
50
 
32
- command = argv.shift
33
51
  case command
34
52
  when "load"
35
53
  run_load!(argv, options)
@@ -45,8 +63,19 @@ module Vivarium
45
63
  abort "Usage: vivarium load <script>" unless script
46
64
  abort "File not found: #{script}" unless File.exist?(script)
47
65
 
66
+ filter = Vivarium::DEFAULT_FILTER
67
+ filter = filter.merge(dedup_values: true) if options[:dedup_values]
68
+
69
+ endpoint = options[:otel_endpoint] || ENV["OTEL_EXPORTER_OTLP_ENDPOINT"]
70
+ otel_out = options[:otel_out]
71
+ if endpoint && otel_out
72
+ warn "[vivarium] --otel-endpoint takes precedence; ignoring --otel-out"
73
+ otel_out = nil
74
+ end
75
+
48
76
  Vivarium.observe(socket_path: options[:socket_path], dest: options[:dest],
49
- filter: Vivarium::DEFAULT_FILTER, save_raw: options[:save_raw]) do
77
+ filter: filter, save_raw: options[:save_raw],
78
+ otel_out: otel_out, otel_endpoint: endpoint) do
50
79
  Kernel.load(File.expand_path(script))
51
80
  end
52
81
  end
@@ -63,10 +92,19 @@ module Vivarium
63
92
  abort "Invalid vivarium-raw file #{raw}: #{e.message}"
64
93
  end
65
94
  meta = data[:meta]
95
+
96
+ if options[:dump_otel]
97
+ dump_otel(data[:events], options[:dest])
98
+ return
99
+ end
100
+
66
101
  filter = resolve_report_filter(options)
67
102
  if options[:max_span_depth]
68
103
  filter = (filter || {}).merge(max_span_depth: options[:max_span_depth])
69
104
  end
105
+ if options[:dedup_values]
106
+ filter = (filter || {}).merge(dedup_values: true)
107
+ end
70
108
 
71
109
  Vivarium::TreeRenderer.new(
72
110
  events: data[:events],
@@ -81,6 +119,63 @@ module Vivarium
81
119
  ).render
82
120
  end
83
121
 
122
+ # Flat per-event dump of the OTel-oriented fields, sorted by ktime, with
123
+ # method-call span nesting resolved by compute_otel_rows. An alternative to
124
+ # the tree view for inspecting trace_id/span_id propagation.
125
+ def self.dump_otel(events, dest)
126
+ header = format("%-18s %-7s %-7s %-6s %-6s %-16s %-32s %-16s %-16s %-5s %s",
127
+ "ktime_ns", "pid", "tid", "uid", "gid", "comm",
128
+ "trace_id", "span_id", "parent_span", "depth", "event")
129
+ dest.puts header
130
+ compute_otel_rows(events).each do |e, span, parent, depth|
131
+ trace = format("%016x%016x", e.trace_hi.to_i, e.trace_lo.to_i)
132
+ dest.puts format("%-18d %-7d %-7d %-6d %-6d %-16s %-32s %016x %016x %-5d %s",
133
+ e.ktime_ns, e.pid, e.tid, e.uid.to_i, e.gid.to_i, e.comm.to_s,
134
+ trace, span, parent, depth, e.event_name)
135
+ end
136
+ end
137
+
138
+ # Resolve each event's effective OTel span by replaying span_start/span_stop
139
+ # into a per-tid stack of method-call spans on top of the BPF-provided
140
+ # thread/process span. Returns [event, span_id, parent_span_id, depth] per
141
+ # event. A method span's id is hashed from (trace_id, tid, span-start ktime):
142
+ # non-zero, unique within the trace, and stable across re-runs. When no method
143
+ # span is active, the thread span and its parent_span_id form the base frame.
144
+ # The stack is per-tid so spawned children nest independently.
145
+ def self.compute_otel_rows(events)
146
+ sorted = events.sort_by { |e| [e.ktime_ns, e.pid, e.tid] }
147
+ stacks = Hash.new { |h, k| h[k] = [] } # tid => [method_span_id, ...]
148
+ parent_of = {} # method_span_id => parent span_id
149
+
150
+ sorted.map do |e|
151
+ thread_span = e.span_id.to_i
152
+ stack = stacks[e.tid]
153
+
154
+ case e.event_name
155
+ when "span_start"
156
+ parent = stack.empty? ? thread_span : stack.last
157
+ span = Vivarium.synth_span_id(e.trace_hi.to_i, e.trace_lo.to_i, e.tid, e.ktime_ns)
158
+ parent_of[span] = parent
159
+ stack.push(span)
160
+ [e, span, parent, stack.size]
161
+ when "span_stop"
162
+ if stack.empty?
163
+ [e, thread_span, e.parent_span_id.to_i, 0]
164
+ else
165
+ span = stack.pop
166
+ [e, span, parent_of[span] || thread_span, stack.size + 1]
167
+ end
168
+ else
169
+ if stack.empty?
170
+ [e, thread_span, e.parent_span_id.to_i, 0]
171
+ else
172
+ span = stack.last
173
+ [e, span, parent_of[span] || thread_span, stack.size]
174
+ end
175
+ end
176
+ end
177
+ end
178
+
84
179
  # Resolve the report display filter by precedence:
85
180
  # --all > --filter JSON > --event NAMES > DEFAULT_FILTER
86
181
  def self.resolve_report_filter(options)
@@ -14,13 +14,15 @@ module Vivarium
14
14
  SAVE_RAW_PROGRESS_INTERVAL = 1000
15
15
 
16
16
  def initialize(socket_path: Vivarium.socket_path, observer_pid:, main_tid:,
17
- filter: nil, dest: $stdout, save_raw: nil)
17
+ filter: nil, dest: $stdout, save_raw: nil, otel_out: nil, otel_endpoint: nil)
18
18
  @socket_path = socket_path
19
19
  @observer_pid = observer_pid
20
20
  @main_tid = main_tid
21
21
  @filter = filter
22
22
  @dest = dest
23
23
  @save_raw = save_raw
24
+ @otel_out = otel_out
25
+ @otel_endpoint = otel_endpoint
24
26
 
25
27
  @client = DaemonClient.new(socket_path: socket_path)
26
28
  @events = []
@@ -35,6 +37,7 @@ module Vivarium
35
37
 
36
38
  @session_start_iso = Time.now.utc.iso8601(3)
37
39
  @session_start_ktime = Vivarium.monotonic_ktime_ns
40
+ start_stream_exporter if @otel_endpoint
38
41
  @sock = @client.open_event_stream
39
42
  @thread = Thread.new { run }
40
43
  @started = true
@@ -51,6 +54,14 @@ module Vivarium
51
54
  @session_stop_iso = Time.now.utc.iso8601(3)
52
55
  @session_stop_ktime = Vivarium.monotonic_ktime_ns
53
56
 
57
+ if @stream
58
+ @stream.finalize(stop_ktime: @session_stop_ktime)
59
+ @stream_exporter.shutdown
60
+ warn "[vivarium] otel: streamed spans -> #{@otel_endpoint}"
61
+ @stopped = true
62
+ return
63
+ end
64
+
54
65
  events_snapshot = @events_mutex.synchronize { @events.dup }
55
66
  @stopped = true
56
67
 
@@ -71,6 +82,14 @@ module Vivarium
71
82
  return
72
83
  end
73
84
 
85
+ if @otel_out
86
+ File.open(@otel_out, "w") do |io|
87
+ Vivarium::OtelExporter.dump(io, events: events_snapshot, meta: meta)
88
+ end
89
+ warn "[vivarium] otel_out: wrote OTLP spans -> #{@otel_out}"
90
+ return
91
+ end
92
+
74
93
  TreeRenderer.new(events: events_snapshot, **meta, filter: @filter, dest: @dest).render
75
94
  end
76
95
 
@@ -113,8 +132,27 @@ module Vivarium
113
132
  buffer
114
133
  end
115
134
 
135
+ def start_stream_exporter
136
+ @stream_exporter = Vivarium::OtelHttpExporter.new(endpoint: @otel_endpoint).start
137
+ @stream = Vivarium::OtelSpanStreamer.new(
138
+ exporter: @stream_exporter,
139
+ session_start_iso: @session_start_iso,
140
+ session_start_ktime: @session_start_ktime,
141
+ observer_pid: @observer_pid,
142
+ main_tid: @main_tid
143
+ )
144
+ end
145
+
116
146
  def capture_event(bytes)
117
147
  ev = Vivarium::RawStore.unpack_record(bytes)
148
+
149
+ # Streaming mode converts events to spans live and does not buffer them,
150
+ # so a resident observation does not grow memory without bound.
151
+ if @stream
152
+ @stream.on_event(ev)
153
+ return
154
+ end
155
+
118
156
  count = @events_mutex.synchronize { @events << ev; @events.size }
119
157
  report_save_progress(count)
120
158
  rescue StandardError => e
@@ -5,7 +5,7 @@ require "set"
5
5
  module Vivarium
6
6
  class DisplayFilter
7
7
  attr_reader :include_events, :exclude_events, :include_severities, :include_pids, :include_tids,
8
- :max_span_depth
8
+ :max_span_depth, :dedup_values
9
9
 
10
10
  def self.compile(raw)
11
11
  return new if raw.nil?
@@ -30,6 +30,7 @@ module Vivarium
30
30
  @include_span_names = normalize_string_set(fetch_key(:include_span_names, :span_names))
31
31
  @span_pattern = normalize_pattern(fetch_key(:span, :span_pattern))
32
32
  @max_span_depth = normalize_integer(fetch_key(:max_span_depth, :max_depth))
33
+ @dedup_values = normalize_bool(fetch_key(:dedup_values, :dedup))
33
34
 
34
35
  payload_value = fetch_key(:payload)
35
36
  @payload_pattern = normalize_pattern(fetch_key(:payload_pattern))
@@ -46,7 +47,7 @@ module Vivarium
46
47
  end
47
48
 
48
49
  def needs_payload?
49
- !@payload_pattern.nil? || !@payload_patterns_by_event.empty?
50
+ !@payload_pattern.nil? || !@payload_patterns_by_event.empty? || @dedup_values
50
51
  end
51
52
 
52
53
  def allow_span_name?(span_name)
@@ -139,6 +140,16 @@ module Vivarium
139
140
  nil
140
141
  end
141
142
 
143
+ def normalize_bool(value)
144
+ case value
145
+ when true then true
146
+ when false, nil then false
147
+ when String then %w[1 true yes on].include?(value.strip.downcase)
148
+ when Numeric then !value.zero?
149
+ else true
150
+ end
151
+ end
152
+
142
153
  def normalize_pattern(value)
143
154
  case value
144
155
  when nil
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Vivarium
7
+ # Converts a captured event stream into an OTLP/JSON ResourceSpans document.
8
+ #
9
+ # Two span layers are emitted (see plan): a base thread/process span per tid
10
+ # (identified by the BPF-issued span_id) and method-call spans reconstructed
11
+ # from span_start/span_stop. Every other event becomes an OTel span event on
12
+ # the innermost active span. Method span ids come from Vivarium.synth_span_id
13
+ # so they match the report --dump-otel view.
14
+ module OtelExporter
15
+ SPAN_KIND_INTERNAL = 1
16
+ SERVICE_NAME = "vivarium"
17
+ INTERNAL_COMM_MATCH = [/otel_stream\.rb/, /correlator\.rb/].freeze
18
+ CONTROL_EVENT_NAMES = %w[proc_exit].freeze
19
+
20
+ module_function
21
+
22
+ # io: a writable IO. Writes a single-line OTLP/JSON document.
23
+ def dump(io, events:, meta:)
24
+ io.write(JSON.generate(build_document(events: events, meta: meta)))
25
+ end
26
+
27
+ def build_document(events:, meta:)
28
+ wrap_document(build_spans(events: events, meta: meta))
29
+ end
30
+
31
+ # Wraps a list of OTLP span hashes in the ResourceSpans envelope. Shared by
32
+ # the batch file exporter and the streaming HTTP exporter so both emit the
33
+ # same resource/scope.
34
+ def wrap_document(spans)
35
+ {
36
+ resourceSpans: [
37
+ {
38
+ resource: { attributes: [str_attr("service.name", SERVICE_NAME)] },
39
+ scopeSpans: [
40
+ {
41
+ scope: { name: SERVICE_NAME, version: Vivarium::VERSION },
42
+ spans: spans
43
+ }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ end
49
+
50
+ # Returns an array of OTLP span hashes.
51
+ def build_spans(events:, meta:)
52
+ sorted = events.sort_by { |e| [e.ktime_ns, e.pid, e.tid] }
53
+ start_ktime = meta[:session_start_ktime].to_i
54
+ stop_ktime = meta[:session_stop_ktime].to_i
55
+ start_unix = iso_to_unix_ns(meta[:session_start_iso])
56
+ main_tid = meta[:main_tid]
57
+ to_unix = ->(k) { (start_unix + (k.to_i - start_ktime)).to_s }
58
+
59
+ thread_spans = {} # tid => mutable span record
60
+ method_spans = [] # all method span records
61
+ stacks = Hash.new { |h, k| h[k] = [] } # tid => [method span record, ...]
62
+
63
+ sorted.each do |ev|
64
+ next if internal_comm?(ev.comm)
65
+ next if control_event?(ev.event_name)
66
+
67
+ ts = (thread_spans[ev.tid] ||= new_thread_span(ev, main_tid))
68
+ ts[:comm] = ev.comm.to_s unless ev.comm.to_s.empty?
69
+ ts[:min_k] = ev.ktime_ns if ev.ktime_ns < ts[:min_k]
70
+ ts[:max_k] = ev.ktime_ns if ev.ktime_ns > ts[:max_k]
71
+ stack = stacks[ev.tid]
72
+
73
+ case ev.event_name
74
+ when "span_start"
75
+ name, file, lineno = read_span_payload(ev.payload)
76
+ parent = stack.empty? ? ts[:span_id] : stack.last[:span_id]
77
+ rec = {
78
+ tid: ev.tid, pid: ev.pid,
79
+ span_id: Vivarium.synth_span_id(ev.trace_hi.to_i, ev.trace_lo.to_i, ev.tid, ev.ktime_ns),
80
+ trace_hi: ev.trace_hi.to_i, trace_lo: ev.trace_lo.to_i,
81
+ parent: parent, name: (name.nil? || name.empty? ? "<anonymous>" : name),
82
+ file: file, lineno: lineno, start_k: ev.ktime_ns, stop_k: nil, events: []
83
+ }
84
+ method_spans << rec
85
+ stack.push(rec)
86
+ when "span_stop"
87
+ rec = stack.pop
88
+ rec[:stop_k] = ev.ktime_ns if rec
89
+ else
90
+ host = stack.empty? ? ts : stack.last
91
+ host[:events] << build_span_event(ev, to_unix)
92
+ end
93
+ end
94
+
95
+ method_spans.each { |rec| rec[:stop_k] ||= stop_ktime }
96
+
97
+ out = []
98
+ thread_spans.each_value { |ts| out << thread_span_hash(ts, start_ktime, stop_ktime, to_unix) }
99
+ method_spans.each { |rec| out << method_span_hash(rec, to_unix) }
100
+ out
101
+ end
102
+
103
+ # --- span record construction -------------------------------------------
104
+
105
+ def new_thread_span(ev, main_tid)
106
+ span_id = ev.span_id.to_i
107
+ span_id = Vivarium.synth_span_id(ev.trace_hi.to_i, ev.trace_lo.to_i, ev.tid, ev.ktime_ns) if span_id.zero?
108
+ {
109
+ tid: ev.tid, pid: ev.pid, span_id: span_id,
110
+ trace_hi: ev.trace_hi.to_i, trace_lo: ev.trace_lo.to_i,
111
+ parent: ev.parent_span_id.to_i, comm: ev.comm.to_s,
112
+ root: ev.tid == main_tid,
113
+ min_k: ev.ktime_ns, max_k: ev.ktime_ns, events: []
114
+ }
115
+ end
116
+
117
+ def thread_span_hash(ts, start_ktime, stop_ktime, to_unix)
118
+ start_k = ts[:root] ? start_ktime : ts[:min_k]
119
+ stop_k = ts[:root] ? stop_ktime : ts[:max_k]
120
+ name = ts[:comm].empty? ? "tid=#{ts[:tid]}" : ts[:comm]
121
+ span_hash(
122
+ trace_hi: ts[:trace_hi], trace_lo: ts[:trace_lo],
123
+ span_id: ts[:span_id], parent: ts[:parent], name: name,
124
+ start_k: start_k, stop_k: stop_k, to_unix: to_unix,
125
+ attributes: [
126
+ int_attr("thread.id", ts[:tid]),
127
+ int_attr("process.pid", ts[:pid]),
128
+ str_attr("process.command", ts[:comm])
129
+ ],
130
+ events: ts[:events] || []
131
+ )
132
+ end
133
+
134
+ def method_span_hash(rec, to_unix)
135
+ attrs = [int_attr("thread.id", rec[:tid]), int_attr("process.pid", rec[:pid])]
136
+ attrs << str_attr("code.filepath", rec[:file]) if rec[:file] && !rec[:file].empty?
137
+ attrs << int_attr("code.lineno", rec[:lineno]) if rec[:lineno] && rec[:lineno] > 0
138
+ span_hash(
139
+ trace_hi: rec[:trace_hi], trace_lo: rec[:trace_lo],
140
+ span_id: rec[:span_id], parent: rec[:parent], name: rec[:name],
141
+ start_k: rec[:start_k], stop_k: rec[:stop_k], to_unix: to_unix,
142
+ attributes: attrs, events: rec[:events]
143
+ )
144
+ end
145
+
146
+ def span_hash(trace_hi:, trace_lo:, span_id:, parent:, name:, start_k:, stop_k:, to_unix:, attributes:, events:)
147
+ hash = {
148
+ traceId: hex32(trace_hi, trace_lo),
149
+ spanId: hex16(span_id),
150
+ name: name,
151
+ kind: SPAN_KIND_INTERNAL,
152
+ startTimeUnixNano: to_unix.call(start_k),
153
+ endTimeUnixNano: to_unix.call(stop_k),
154
+ attributes: attributes
155
+ }
156
+ hash[:parentSpanId] = hex16(parent) unless parent.to_i.zero?
157
+ hash[:events] = events unless events.empty?
158
+ hash
159
+ end
160
+
161
+ def build_span_event(ev, to_unix)
162
+ { timeUnixNano: to_unix.call(ev.ktime_ns), name: ev.event_name, attributes: event_attributes(ev) }
163
+ end
164
+
165
+ # OTLP attribute list describing an event (target/severity/uid/gid/comm/...).
166
+ # Reused for both span events and standalone single-event spans (streaming).
167
+ def event_attributes(ev)
168
+ target =
169
+ begin
170
+ Vivarium.render_event_payload(ev).to_s.gsub(/\s+/, " ").strip
171
+ rescue StandardError
172
+ ""
173
+ end
174
+ attrs = [
175
+ int_attr("thread.id", ev.tid),
176
+ int_attr("process.pid", ev.pid),
177
+ int_attr("user.id", ev.uid),
178
+ int_attr("group.id", ev.gid),
179
+ str_attr("severity", Vivarium.event_severity(ev.event_name))
180
+ ]
181
+ attrs << str_attr("process.command", ev.comm.to_s) unless ev.comm.to_s.empty?
182
+ attrs << str_attr("target", target) unless target.empty?
183
+ attrs
184
+ end
185
+
186
+ # --- helpers -------------------------------------------------------------
187
+
188
+ def internal_comm?(comm)
189
+ value = comm.to_s
190
+ INTERNAL_COMM_MATCH.any? { |regex| value.match?(regex) }
191
+ end
192
+
193
+ def control_event?(event_name)
194
+ CONTROL_EVENT_NAMES.include?(event_name.to_s)
195
+ end
196
+
197
+ def read_span_payload(payload)
198
+ bytes = payload.to_s.b
199
+ return [nil, nil, -1] if bytes.empty?
200
+
201
+ name = Vivarium.c_string(bytes[0, Vivarium::SPAN_METHOD_SIZE])
202
+ file = Vivarium.c_string(bytes[Vivarium::SPAN_METHOD_SIZE, Vivarium::SPAN_FILE_SIZE])
203
+ lineno = bytes.bytesize > Vivarium::SPAN_LINENO_OFFSET ? bytes[Vivarium::SPAN_LINENO_OFFSET, 8].unpack1("q<") : -1
204
+ [name, file, lineno]
205
+ end
206
+
207
+ def iso_to_unix_ns(iso)
208
+ return 0 if iso.nil? || iso.to_s.empty?
209
+
210
+ (Time.iso8601(iso).to_r * 1_000_000_000).to_i
211
+ rescue ArgumentError
212
+ 0
213
+ end
214
+
215
+ def hex32(hi, lo)
216
+ format("%016x%016x", hi.to_i & Vivarium::U64_MASK, lo.to_i & Vivarium::U64_MASK)
217
+ end
218
+
219
+ def hex16(value)
220
+ format("%016x", value.to_i & Vivarium::U64_MASK)
221
+ end
222
+
223
+ def str_attr(key, value)
224
+ { key: key, value: { stringValue: value.to_s } }
225
+ end
226
+
227
+ def int_attr(key, value)
228
+ { key: key, value: { intValue: value.to_i.to_s } }
229
+ end
230
+ end
231
+ end