vivarium 0.3.0 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8bbb6affa1c85f3c5af82f751021cc6d0230741545bcb090a3441ae6285ddae
4
- data.tar.gz: 0a51c1fd7065cf030363679f8f1c2370937365b0d22115326948cabb6053ded4
3
+ metadata.gz: a0e7e80cef690342f163fde6c05fed6859c3ef81aa794fe532c87b00fbe11cd5
4
+ data.tar.gz: 4f2de93c9795fe93ecb0d8291f86a117ac0f595c8c12e2c7b41caabaae7c3273
5
5
  SHA512:
6
- metadata.gz: 4efc19c34686b652e07213be2c6d865fa9b02fbe0a207b1f9e391cfd8591269f8dafbe6ad7c444091541bd1cb10e2c8a6ae07564caa5fe5deb2ed38226b3a38c
7
- data.tar.gz: e0f15247ae4ed3ff159935686affd554626eb079bda7cf1db9e5f577df93db5ab57134c87ec926cab96da8302990c7524f187bbf120559b386cc40fe8399807f
6
+ metadata.gz: 9a116f2c50b978a368a05cca351411f9724686746820085b651038d060c8452a8546170cb6e6ac885ab8a721aa20bcf42c5746639d32cbfea0ba8a9452b9687e
7
+ data.tar.gz: fb5b4b5307682fa84b77dcfaa2165a71a5e16deef8d7b7d21d6b3267e82b5a12ce1f73dee946193605e039a247d59613a9c2dffca15ef96ddb502a17c9ce742d
data/README.md CHANGED
@@ -21,6 +21,7 @@ Implemented in this repository:
21
21
  - BPF LSM hooks on `inode_symlink`, `inode_link`, `inode_rename`, `path_chmod`
22
22
  - BPF tracepoint on `sys_enter_getdents64`
23
23
  - BPF tracepoint on `sys_enter_execve` (captures executable path and first few argv entries as `proc_exec`)
24
+ - BPF tracepoint on `sched_process_fork` (tracks descendants and emits `proc_fork`)
24
25
  - BPF LSM hooks for suspicious behavior checks:
25
26
  - `ptrace_access_check` (emits `ptrace_check`)
26
27
  - `sb_mount` (emits `sb_mount`)
@@ -32,17 +33,18 @@ Implemented in this repository:
32
33
  - BPF LSM hook on `socket_create` (flags unusual socket creation as `odd_socket`)
33
34
  - BPF LSM hook on `socket_connect` (captures destination family/address/port as `sock_connect`)
34
35
  - BPF tracepoints on `sys_enter_sendmsg`, `sys_enter_sendto`, `sys_enter_sendmmsg` (capture UDP/53 DNS QNAME raw bytes as `dns_req`)
36
+ - USDT probes for method span boundaries and exceptions (`span_start`, `span_stop`, `span_raise`)
37
+ - OpenSSL `SSL_write` uprobe event (`ssl_write`)
35
38
  - Shared pinned maps on bpffs
36
39
  - `config_root_targets` (root PID -> 0/1)
37
40
  - `config_spawned_targets` (spawned TID -> 0/1)
38
- - `event_invoked` (array length 1024 with `event_t` records)
39
- - `event_write_pos` (cursor for appending into `event_invoked`)
41
+ - `events` (`BPF_RINGBUF_OUTPUT`, shared by system events and span events)
40
42
  - Ruby API `Vivarium.observe do ... end`
41
43
  - Registers current PID to `config_root_targets`
42
44
  - eBPF tracks spawned descendants into `config_spawned_targets` via `sched_process_fork`
43
- - On each `:return` / `:c_return`, drains `event_invoked`
44
- - Prints stack trace + events
45
- - Clears event slots and cursor
45
+ - `TracePoint` emits span probes on allowlisted call/return and emits `span_raise` on Ruby `:raise`
46
+ - Correlator thread consumes ringbuf events and joins them to spans by `tid`/time window
47
+ - Renders a process tree once at session end
46
48
  - Unregisters PID on block exit
47
49
 
48
50
  `event_t` currently:
@@ -51,6 +53,7 @@ Implemented in this repository:
51
53
  struct event_t {
52
54
  u64 ktime_ns;
53
55
  u32 pid;
56
+ u32 tid;
54
57
  char event_name[16];
55
58
  char payload[256];
56
59
  };
@@ -147,7 +150,7 @@ observer = Vivarium.top_observe
147
150
  observer.stop
148
151
  ```
149
152
 
150
- By default, Vivarium excludes its own internal frames from stack output. Set `VIVARIUM_FILTER_INTERNAL_FRAMES=0` to disable this filter.
153
+ `Vivarium.observe` / `Vivarium.top_observe` produce one process-tree report at session end (block exit, `observer.stop`, or process exit).
151
154
 
152
155
  You can override pin directory via `VIVARIUM_BPF_PIN_DIR` on both sides:
153
156
 
@@ -179,17 +182,18 @@ bundle exec vivariumd --pin-dir /sys/fs/bpf/vivarium
179
182
  ## Notes
180
183
 
181
184
  - Thread/Ractor-awareness is not yet implemented.
182
- - `event_invoked` uses fixed 1024 slots and wraps around when full.
185
+ - Current transport is ring buffer (`events`) pinned under bpffs.
186
+ - Ring buffer is single-consumer by nature; v1 supports a single observer per host.
183
187
  - `payload` is 256 bytes in `event_t`; some event types intentionally use smaller structured slices inside that buffer.
184
188
  - `proc_exec` currently stores the executable path plus up to 3 argv entries in 4 fixed 64-byte slots to keep the BPF verifier happy.
189
+ - `span_raise` is emitted on Ruby `:raise` and rendered as `EXCP` lines within the enclosing span.
190
+ - Events that do not match any real span are grouped into synthetic `<no-span>` spans.
185
191
  - Each event is tagged with severity metadata: `high` for `setid_change`, `capable_check`, `bprm_creds`, `task_kill`, `ptrace_check`, `sb_mount`, and `kernel_read_file`; others are `medium` by default.
186
- - In `human` format output, `high` severity events are rendered in red.
187
192
  - `capable_check` is intentionally filtered to high-risk capabilities to reduce noise from extremely frequent `capable` hook calls.
188
- - Current output format is textual and intended for iteration.
193
+ - Output format is textual process tree with a session header and per-span relative timing.
189
194
  - `vivariumd` resolves `struct file::f_path` offset from `/sys/kernel/btf/vmlinux` at startup.
190
195
  - `vivariumd` also resolves `struct dentry::d_name` offset from `/sys/kernel/btf/vmlinux` at startup.
191
196
  - You can override offsets manually with `VIVARIUM_FILE_F_PATH_OFFSET` and `VIVARIUM_DENTRY_D_NAME_OFFSET` if auto-detection fails.
192
- - `vivariumd` also prints `bpf_trace_printk` lines (`vivarium: pid=... path=...`) to its own logs.
193
197
 
194
198
  ## Contributing
195
199
 
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fiddle"
5
+ require "vivarium"
6
+
7
+ FILTER = {
8
+ include_events: %w[dlopen mmap_exec]
9
+ }.freeze
10
+
11
+ # Usage:
12
+ # 1) In another shell (root): sudo bundle exec vivariumd
13
+ # (dlopen uprobe is attached automatically when libc is found)
14
+ # 2) Run this script: bundle exec ruby examples/dlopen_demo.rb
15
+ #
16
+ # Expected output: "DL dlopen" and "DL mmap_exec" events for each
17
+ # library loaded via Fiddle.dlopen.
18
+ #
19
+ # You can disable the dlopen uprobe with `sudo vivariumd --no-dlopen-trace`
20
+ # or point at a specific libc with `sudo vivariumd --libc /lib/x86_64-linux-gnu/libc.so.6`.
21
+
22
+ Vivarium.observe(filter: FILTER) do
23
+ # libm: math functions — almost universally available
24
+ begin
25
+ libm = Fiddle.dlopen("libm.so.6")
26
+ sin_fn = Fiddle::Function.new(libm["sin"], [Fiddle::TYPE_DOUBLE], Fiddle::TYPE_DOUBLE)
27
+ puts "[dlopen_demo] sin(PI/4) = #{sin_fn.call(Math::PI / 4).round(6)}"
28
+ libm.close
29
+ rescue Fiddle::DLError => e
30
+ warn "[dlopen_demo] libm: #{e.message}"
31
+ end
32
+
33
+ # libsqlite3: a common library that may not be loaded at startup
34
+ begin
35
+ libsqlite3 = Fiddle.dlopen("libsqlite3.so.0")
36
+ puts "[dlopen_demo] libsqlite3 loaded: version = #{Fiddle::Function.new(libsqlite3["sqlite3_libversion"], [], Fiddle::TYPE_VOIDP).call}"
37
+ libsqlite3.close
38
+ rescue Fiddle::DLError => e
39
+ warn "[dlopen_demo] libsqlite3: #{e.message}"
40
+ end
41
+
42
+ # Spawn a child process that also calls dlopen — its events should
43
+ # appear under a PROC node in the tree (descendant PID tracking).
44
+ # Unbundle so Bundler does not spawn anything.
45
+ Bundler.with_unbundled_env do
46
+ system("ruby -e 'require \"fiddle\"; Fiddle.dlopen(\"libm.so.6\").close'")
47
+ end
48
+ end
49
+
50
+ puts "[dlopen_demo] done"
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Standalone demo: shows what DROP warning nodes look like in the TreeRenderer
5
+ # output. Does NOT require BPF or vivariumd — constructs RawEvent objects
6
+ # directly and feeds them to TreeRenderer.
7
+ #
8
+ # Usage:
9
+ # ruby examples/drop_demo.rb
10
+
11
+ $LOAD_PATH.unshift File.join(__dir__, "..", "lib")
12
+
13
+ require "vivarium"
14
+ require "vivarium/correlator"
15
+ require "vivarium/tree_renderer"
16
+ require "vivarium/display_filter"
17
+ require "vivarium_usdt"
18
+
19
+ t0 = 1_000_000_000 # base ktime_ns
20
+ pid = Process.pid
21
+ tid = Process.pid
22
+ method_id = 0x0001_0001
23
+
24
+ # span_start payload: method_id (8B) + file_id (8B) + lineno (8B)
25
+ span_start_payload = [method_id, 0, 10].pack("q<q<q<")
26
+ .ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00")
27
+
28
+ events = [
29
+ Vivarium::Correlator::RawEvent.new(
30
+ ktime_ns: t0,
31
+ pid: pid, tid: tid,
32
+ event_name: "span_start",
33
+ payload: span_start_payload,
34
+ dropped_since_last: 0
35
+ ),
36
+ # This event carries drop info: 5 events were lost before it arrived
37
+ Vivarium::Correlator::RawEvent.new(
38
+ ktime_ns: t0 + 10_000_000,
39
+ pid: pid, tid: tid,
40
+ event_name: "path_open",
41
+ payload: "/etc/passwd\x00".b.ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00"),
42
+ dropped_since_last: 5
43
+ ),
44
+ Vivarium::Correlator::RawEvent.new(
45
+ ktime_ns: t0 + 20_000_000,
46
+ pid: pid, tid: tid,
47
+ event_name: "dns_req",
48
+ payload: "\x06google\x03com\x00".b.ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00"),
49
+ dropped_since_last: 0
50
+ ),
51
+ # Another burst: 12 events dropped just before this sock_connect
52
+ Vivarium::Correlator::RawEvent.new(
53
+ ktime_ns: t0 + 25_000_000,
54
+ pid: pid, tid: tid,
55
+ event_name: "sock_connect",
56
+ payload: [2, 443, 0x7f000001, 0].pack("S<nNN").ljust(Vivarium::EVENT_PAYLOAD_SIZE, "\x00"),
57
+ dropped_since_last: 12
58
+ ),
59
+ Vivarium::Correlator::RawEvent.new(
60
+ ktime_ns: t0 + 30_000_000,
61
+ pid: pid, tid: tid,
62
+ event_name: "span_stop",
63
+ payload: "\x00" * Vivarium::EVENT_PAYLOAD_SIZE,
64
+ dropped_since_last: 0
65
+ ),
66
+ ]
67
+
68
+ Vivarium::TreeRenderer.new(
69
+ events: events,
70
+ method_table: { method_id => "MyClass#my_method" },
71
+ observer_pid: pid,
72
+ main_tid: tid,
73
+ session_start_iso: "2026-06-02T00:00:00.000Z",
74
+ session_start_ktime: t0,
75
+ session_stop_iso: "2026-06-02T00:00:00.030Z",
76
+ session_stop_ktime: t0 + 30_000_000,
77
+ dest: $stdout
78
+ ).render
@@ -9,6 +9,9 @@ require "vivarium"
9
9
  # 2) Run this script: bundle exec ruby examples/execve_demo.rb
10
10
 
11
11
  TMP_PREFIX = "vivarium-exec-demo"
12
+ FILTER = {
13
+ include_events: %w[proc_exec]
14
+ }.freeze
12
15
 
13
16
  def try_step(title)
14
17
  puts "[exec-demo] #{title}"
@@ -20,7 +23,7 @@ end
20
23
  Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
21
24
  output_path = File.join(dir, "execve-demo.out")
22
25
 
23
- Vivarium.observe do
26
+ Vivarium.observe(filter: FILTER) do
24
27
  try_step("system echo with multiple args") do
25
28
  system("/bin/echo", "hello", "from", "vivarium", out: File::NULL)
26
29
  end
@@ -10,6 +10,9 @@ require "vivarium"
10
10
  # 2) Run this script: bundle exec ruby examples/file_operation_demo.rb
11
11
 
12
12
  TMP_PREFIX = "vivarium-file-demo"
13
+ FILTER = {
14
+ include_events: %w[path_open file_symlink file_hardlink file_rename file_chmod file_getdents]
15
+ }.freeze
13
16
 
14
17
  def try_step(title)
15
18
  puts "[file-demo] #{title}"
@@ -24,7 +27,7 @@ Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
24
27
  hardlink_path = File.join(dir, "hardlink.txt")
25
28
  symlink_path = File.join(dir, "symlink.txt")
26
29
 
27
- Vivarium.observe do
30
+ Vivarium.observe(filter: FILTER) do
28
31
  try_step("create source file") do
29
32
  File.write(source_path, "vivarium sample\n")
30
33
  File.read(source_path)
@@ -4,6 +4,10 @@
4
4
  require "socket"
5
5
  require "vivarium"
6
6
 
7
+ FILTER = {
8
+ include_events: %w[sock_connect dns_req odd_socket]
9
+ }.freeze
10
+
7
11
  # Usage:
8
12
  # 1) In another shell (root): sudo bundle exec vivariumd
9
13
  # 2) Run this script: bundle exec ruby examples/network_client_demo.rb
@@ -15,7 +19,7 @@ rescue StandardError => e
15
19
  puts "[client] #{title} failed: #{e.class}: #{e.message}"
16
20
  end
17
21
 
18
- Vivarium.observe do
22
+ Vivarium.observe(filter: FILTER) do
19
23
  # Likely emits sock_connect and dns_req via resolver traffic.
20
24
  try_step("system: DNS lookup") do
21
25
  system("getent hosts example.com >/dev/null 2>&1 || true")
@@ -3,6 +3,10 @@
3
3
 
4
4
  require "vivarium"
5
5
 
6
+ FILTER = {
7
+ include_events: %w[setid_change capable_check bprm_creds]
8
+ }.freeze
9
+
6
10
  # Usage:
7
11
  # 1) In another shell (root): sudo bundle exec vivariumd
8
12
  # 2) Run this script: bundle exec ruby examples/privilege_event_demo.rb
@@ -14,7 +18,7 @@ rescue StandardError => e
14
18
  puts "[priv-demo] #{title} failed: #{e.class}: #{e.message}"
15
19
  end
16
20
 
17
- Vivarium.observe do
21
+ Vivarium.observe(filter: FILTER) do
18
22
  try_step("attempt setuid(0)") do
19
23
  Process::UID.change_privilege(0)
20
24
  end
@@ -3,6 +3,10 @@
3
3
 
4
4
  require "vivarium"
5
5
 
6
+ FILTER = {
7
+ include_events: %w[span_raise]
8
+ }.freeze
9
+
6
10
  def try_step(title)
7
11
  puts "[priv-demo] #{title}"
8
12
  yield
@@ -10,7 +14,7 @@ rescue StandardError => e
10
14
  puts "[priv-demo] #{title} failed: #{e.class}: #{e.message}"
11
15
  end
12
16
 
13
- Vivarium.observe do
17
+ Vivarium.observe(filter: FILTER) do
14
18
  try_step("raise in main") do
15
19
  raise "error in main"
16
20
  end
@@ -3,6 +3,10 @@
3
3
 
4
4
  require "vivarium"
5
5
 
6
+ FILTER = {
7
+ include_events: %w[task_kill]
8
+ }.freeze
9
+
6
10
  # Usage:
7
11
  # 1) In another shell (root): sudo bundle exec vivariumd
8
12
  # 2) Run this script: bundle exec ruby examples/signal_kill_demo.rb
@@ -16,7 +20,7 @@ end
16
20
 
17
21
  child_pid = nil
18
22
 
19
- Vivarium.observe do
23
+ Vivarium.observe(filter: FILTER) do
20
24
  try_step("fork child process") do
21
25
  child_pid = fork do
22
26
  trap("TERM") { exit!(0) }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "net/http"
5
+ require "uri"
6
+ require "vivarium"
7
+
8
+ FILTER = {
9
+ include_events: %w[ssl_write]
10
+ }.freeze
11
+
12
+ # Usage:
13
+ # 1) In another shell (root): sudo bundle exec vivariumd
14
+ # (SSL_write uprobe is attached automatically when libssl is found)
15
+ # 2) Run this script: bundle exec ruby examples/ssl_write_demo.rb
16
+ #
17
+ # You can disable the SSL_write uprobe with `sudo vivariumd --no-ssl-trace`
18
+ # or point at a specific library with `sudo vivariumd --libssl /path/to/libssl.so.3`.
19
+
20
+ Vivarium.observe(filter: FILTER) do
21
+ # Net::HTTP uses libssl's SSL_write under the hood. With HTTP/1.1 the
22
+ # request line should appear verbatim in the SSL event payload.
23
+ begin
24
+ uri = URI("https://udzura.jp/")
25
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
26
+ http.get(uri.request_uri)
27
+ end
28
+ rescue StandardError => e
29
+ warn "[ssl_demo] Net::HTTP failed: #{e.class}: #{e.message}"
30
+ end
31
+
32
+ # `curl --http2` should produce HTTP/2 traffic; HEADERS frames will be
33
+ # HPACK-decoded if the `http-2` gem is installed on the observer side.
34
+ system("curl -sS --http2 -o /dev/null https://nghttp2.org/ 2>/dev/null")
35
+ end
36
+
37
+ puts "[ssl_demo] done"
@@ -6,7 +6,7 @@ require "time"
6
6
  module Vivarium
7
7
  class Correlator
8
8
  RawEvent = Struct.new(
9
- :ktime_ns, :pid, :tid, :event_name, :payload,
9
+ :ktime_ns, :pid, :tid, :event_name, :payload, :dropped_since_last,
10
10
  keyword_init: true
11
11
  )
12
12
 
@@ -17,16 +17,18 @@ module Vivarium
17
17
  u32 tid;
18
18
  char event_name[16];
19
19
  char payload[256];
20
+ u64 dropped_since_last;
20
21
  };
21
22
  C
22
23
 
23
24
  POLL_TIMEOUT_MS = 200
24
25
 
25
- def initialize(pin_dir:, observer_pid:, main_tid:, method_id_queue:, dest: $stdout)
26
+ def initialize(pin_dir:, observer_pid:, main_tid:, method_id_queue:, filter: nil, dest: $stdout)
26
27
  @pin_dir = pin_dir
27
28
  @observer_pid = observer_pid
28
29
  @main_tid = main_tid
29
30
  @method_id_queue = method_id_queue
31
+ @filter = filter
30
32
  @dest = dest
31
33
 
32
34
  @events = []
@@ -79,6 +81,7 @@ module Vivarium
79
81
  session_start_ktime: @session_start_ktime,
80
82
  session_stop_iso: @session_stop_iso,
81
83
  session_stop_ktime: @session_stop_ktime,
84
+ filter: @filter,
82
85
  dest: @dest
83
86
  ).render
84
87
  end
@@ -102,11 +105,12 @@ module Vivarium
102
105
  bytes = data[0, size].to_s.b
103
106
  bytes = bytes.ljust(Vivarium::EVENT_STRUCT_SIZE, "\x00") if bytes.bytesize < Vivarium::EVENT_STRUCT_SIZE
104
107
 
105
- ktime_ns = bytes[Vivarium::EVENT_TS_OFFSET, Vivarium::EVENT_TS_SIZE].unpack1("Q<")
106
- pid = bytes[Vivarium::EVENT_PID_OFFSET, 4].unpack1("L<")
107
- tid = bytes[Vivarium::EVENT_TID_OFFSET, 4].unpack1("L<")
108
- event_name = Vivarium.c_string(bytes[Vivarium::EVENT_NAME_OFFSET, Vivarium::EVENT_NAME_SIZE])
109
- payload = bytes[Vivarium::EVENT_PAYLOAD_OFFSET, Vivarium::EVENT_PAYLOAD_SIZE].to_s.b
108
+ ktime_ns = bytes[Vivarium::EVENT_TS_OFFSET, Vivarium::EVENT_TS_SIZE].unpack1("Q<")
109
+ pid = bytes[Vivarium::EVENT_PID_OFFSET, 4].unpack1("L<")
110
+ tid = bytes[Vivarium::EVENT_TID_OFFSET, 4].unpack1("L<")
111
+ event_name = Vivarium.c_string(bytes[Vivarium::EVENT_NAME_OFFSET, Vivarium::EVENT_NAME_SIZE])
112
+ payload = bytes[Vivarium::EVENT_PAYLOAD_OFFSET, Vivarium::EVENT_PAYLOAD_SIZE].to_s.b
113
+ dropped_since_last = bytes[Vivarium::EVENT_DROPPED_OFFSET, 8].unpack1("Q<")
110
114
 
111
115
  @events_mutex.synchronize do
112
116
  @events << RawEvent.new(
@@ -114,7 +118,8 @@ module Vivarium
114
118
  pid: pid,
115
119
  tid: tid,
116
120
  event_name: event_name,
117
- payload: payload
121
+ payload: payload,
122
+ dropped_since_last: dropped_since_last
118
123
  )
119
124
  end
120
125
  rescue StandardError => e
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Vivarium
6
+ class DisplayFilter
7
+ attr_reader :include_events, :exclude_events, :include_severities, :include_pids, :include_tids
8
+
9
+ def self.compile(raw)
10
+ return new if raw.nil?
11
+ return raw if raw.is_a?(self)
12
+
13
+ unless raw.respond_to?(:to_h)
14
+ raise ArgumentError, "filter must be a Hash-compatible object"
15
+ end
16
+
17
+ new(raw.to_h)
18
+ end
19
+
20
+ def initialize(raw = {})
21
+ @raw = symbolize_keys(raw || {})
22
+
23
+ @include_events = normalize_string_set(fetch_key(:include_events, :event_names, :events))
24
+ @exclude_events = normalize_string_set(fetch_key(:exclude_events))
25
+ @include_severities = normalize_string_set(fetch_key(:include_severities, :severities, :severity))
26
+ @include_pids = normalize_integer_set(fetch_key(:include_pids, :pids, :pid))
27
+ @include_tids = normalize_integer_set(fetch_key(:include_tids, :tids, :tid))
28
+
29
+ @include_span_names = normalize_string_set(fetch_key(:include_span_names, :span_names))
30
+ @span_pattern = normalize_pattern(fetch_key(:span, :span_pattern))
31
+
32
+ payload_value = fetch_key(:payload)
33
+ @payload_pattern = normalize_pattern(fetch_key(:payload_pattern))
34
+ @payload_patterns_by_event = {}
35
+ if payload_value.is_a?(Hash)
36
+ @payload_patterns_by_event = normalize_payload_map(payload_value)
37
+ else
38
+ @payload_pattern ||= normalize_pattern(payload_value)
39
+ end
40
+ end
41
+
42
+ def enabled?
43
+ !@raw.empty?
44
+ end
45
+
46
+ def needs_payload?
47
+ !@payload_pattern.nil? || !@payload_patterns_by_event.empty?
48
+ end
49
+
50
+ def allow_span_name?(span_name)
51
+ return true if @include_span_names.empty? && @span_pattern.nil?
52
+
53
+ name = span_name.to_s
54
+ return true if @include_span_names.include?(name)
55
+ return true if @span_pattern && @span_pattern.match?(name)
56
+
57
+ false
58
+ end
59
+
60
+ def allow_event?(event_name:, severity:, span_name:, payload: nil, pid: nil, tid: nil)
61
+ return false unless allow_span_name?(span_name)
62
+
63
+ name = event_name.to_s
64
+ sev = severity.to_s
65
+
66
+ return false if @exclude_events.include?(name)
67
+ return false if !@include_events.empty? && !@include_events.include?(name)
68
+ return false if !@include_severities.empty? && !@include_severities.include?(sev)
69
+ return false if !@include_pids.empty? && !@include_pids.include?(pid.to_i)
70
+ return false if !@include_tids.empty? && !@include_tids.include?(tid.to_i)
71
+
72
+ payload_pattern = @payload_patterns_by_event[name] || @payload_pattern
73
+ if payload_pattern
74
+ return false if payload.nil?
75
+ return false unless payload_pattern.match?(payload.to_s)
76
+ end
77
+
78
+ true
79
+ end
80
+
81
+ private
82
+
83
+ def fetch_key(*keys)
84
+ keys.each do |key|
85
+ return @raw[key] if @raw.key?(key)
86
+ end
87
+ nil
88
+ end
89
+
90
+ def symbolize_keys(hash)
91
+ hash.each_with_object({}) do |(k, v), out|
92
+ out[k.respond_to?(:to_sym) ? k.to_sym : k] = v
93
+ end
94
+ end
95
+
96
+ def normalize_string_set(value)
97
+ arr = case value
98
+ when nil
99
+ []
100
+ when Array
101
+ value
102
+ else
103
+ [value]
104
+ end
105
+
106
+ arr.each_with_object(Set.new) do |item, set|
107
+ str = item.to_s.strip
108
+ set << str unless str.empty?
109
+ end
110
+ end
111
+
112
+ def normalize_integer_set(value)
113
+ arr = case value
114
+ when nil
115
+ []
116
+ when Array
117
+ value
118
+ else
119
+ [value]
120
+ end
121
+
122
+ arr.each_with_object(Set.new) do |item, set|
123
+ begin
124
+ set << Integer(item)
125
+ rescue ArgumentError, TypeError
126
+ nil
127
+ end
128
+ end
129
+ end
130
+
131
+ def normalize_pattern(value)
132
+ case value
133
+ when nil
134
+ nil
135
+ when Regexp
136
+ value
137
+ when String
138
+ return nil if value.empty?
139
+
140
+ Regexp.new(Regexp.escape(value))
141
+ else
142
+ nil
143
+ end
144
+ end
145
+
146
+ def normalize_payload_map(raw_map)
147
+ raw_map.each_with_object({}) do |(event_name, pattern), out|
148
+ key = event_name.to_s.strip
149
+ next if key.empty?
150
+
151
+ normalized = normalize_pattern(pattern)
152
+ next unless normalized
153
+
154
+ out[key] = normalized
155
+ end
156
+ end
157
+ end
158
+ end