fiber_audit 0.1.0 → 0.2.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.fiber-audit.example.yml +19 -0
  3. data/ARCHITECTURE.md +726 -0
  4. data/CHANGELOG.md +41 -0
  5. data/LICENSE +201 -0
  6. data/README.md +66 -8
  7. data/lib/fiber_audit/cli.rb +87 -2
  8. data/lib/fiber_audit/configuration.rb +106 -6
  9. data/lib/fiber_audit/errors.rb +2 -0
  10. data/lib/fiber_audit/operation_vocabulary.rb +42 -0
  11. data/lib/fiber_audit/reporters/text.rb +1 -1
  12. data/lib/fiber_audit/runtime/active_operations.rb +146 -0
  13. data/lib/fiber_audit/runtime/boot.rb +83 -0
  14. data/lib/fiber_audit/runtime/clock.rb +35 -0
  15. data/lib/fiber_audit/runtime/environment.rb +289 -0
  16. data/lib/fiber_audit/runtime/event.rb +86 -0
  17. data/lib/fiber_audit/runtime/execution_context.rb +89 -0
  18. data/lib/fiber_audit/runtime/heartbeat.rb +113 -0
  19. data/lib/fiber_audit/runtime/jsonl/schema.rb +312 -0
  20. data/lib/fiber_audit/runtime/jsonl/writer.rb +122 -0
  21. data/lib/fiber_audit/runtime/lifecycle.rb +343 -0
  22. data/lib/fiber_audit/runtime/limits.rb +102 -0
  23. data/lib/fiber_audit/runtime/location.rb +44 -0
  24. data/lib/fiber_audit/runtime/policy.rb +121 -0
  25. data/lib/fiber_audit/runtime/probes/base.rb +333 -0
  26. data/lib/fiber_audit/runtime/probes/http.rb +80 -0
  27. data/lib/fiber_audit/runtime/probes/io_select.rb +50 -0
  28. data/lib/fiber_audit/runtime/probes/registry.rb +156 -0
  29. data/lib/fiber_audit/runtime/probes/socket.rb +76 -0
  30. data/lib/fiber_audit/runtime/probes/subprocess.rb +83 -0
  31. data/lib/fiber_audit/runtime/probes/synchronization.rb +58 -0
  32. data/lib/fiber_audit/runtime/probes/thread_state.rb +39 -0
  33. data/lib/fiber_audit/runtime/probes/thread_wait.rb +25 -0
  34. data/lib/fiber_audit/runtime/rails_integration.rb +320 -0
  35. data/lib/fiber_audit/runtime/recorder.rb +333 -0
  36. data/lib/fiber_audit/runtime/redactor.rb +102 -0
  37. data/lib/fiber_audit/runtime/sampler.rb +26 -0
  38. data/lib/fiber_audit/runtime/scheduler_observer.rb +128 -0
  39. data/lib/fiber_audit/runtime/session.rb +112 -0
  40. data/lib/fiber_audit/runtime/supervisor.rb +114 -0
  41. data/lib/fiber_audit/runtime/validation.rb +68 -0
  42. data/lib/fiber_audit/runtime/watchdog.rb +479 -0
  43. data/lib/fiber_audit/runtime/watchdog_policy.rb +64 -0
  44. data/lib/fiber_audit/runtime.rb +37 -0
  45. data/lib/fiber_audit/static/rules/blocking_subprocess.rb +3 -8
  46. data/lib/fiber_audit/static/rules/direct_socket.rb +2 -3
  47. data/lib/fiber_audit/static/rules/io_select.rb +2 -4
  48. data/lib/fiber_audit/static/rules/net_http_in_request.rb +3 -5
  49. data/lib/fiber_audit/static/rules/synchronization.rb +2 -6
  50. data/lib/fiber_audit/static/rules/thread_current_state.rb +3 -2
  51. data/lib/fiber_audit/static/rules/thread_join.rb +3 -2
  52. data/lib/fiber_audit/version.rb +1 -1
  53. data/lib/fiber_audit.rb +1 -0
  54. metadata +38 -2
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberAudit
4
+ module Runtime
5
+ module Probes
6
+ module Subprocess
7
+ module KernelInstanceHook
8
+ def system(...)
9
+ Registry.observe(operation: 'Kernel.system') { super }
10
+ end
11
+
12
+ def exec(...)
13
+ Registry.observe(operation: 'Kernel.exec', emit_start: true) { super }
14
+ end
15
+
16
+ def spawn(...)
17
+ Registry.observe(operation: 'Kernel.spawn') { super }
18
+ end
19
+
20
+ private :system, :exec, :spawn
21
+ end
22
+
23
+ module KernelSingletonHook
24
+ def system(...)
25
+ Registry.observe(operation: 'Kernel.system') { super }
26
+ end
27
+
28
+ def exec(...)
29
+ Registry.observe(operation: 'Kernel.exec', emit_start: true) { super }
30
+ end
31
+
32
+ def spawn(...)
33
+ Registry.observe(operation: 'Kernel.spawn') { super }
34
+ end
35
+ end
36
+
37
+ module IOHook
38
+ def popen(...)
39
+ Registry.observe(operation: 'IO.popen') { super }
40
+ end
41
+ end
42
+
43
+ module ProcessHook
44
+ def waitall(...)
45
+ Registry.observe(operation: 'Process.waitall') { super }
46
+ end
47
+
48
+ def detach(...)
49
+ Registry.observe(operation: 'Process.detach') { super }
50
+ end
51
+ end
52
+
53
+ module Open3Hook
54
+ def capture2(...)
55
+ Registry.observe(operation: 'Open3.capture2') { super }
56
+ end
57
+
58
+ def capture2e(...)
59
+ Registry.observe(operation: 'Open3.capture2e') { super }
60
+ end
61
+
62
+ def capture3(...)
63
+ Registry.observe(operation: 'Open3.capture3') { super }
64
+ end
65
+
66
+ def pipeline(...)
67
+ Registry.observe(operation: 'Open3.pipeline') { super }
68
+ end
69
+ end
70
+
71
+ module_function
72
+
73
+ def install!(registry)
74
+ registry.prepend_once(Kernel, KernelInstanceHook)
75
+ registry.prepend_once(Kernel.singleton_class, KernelSingletonHook)
76
+ registry.prepend_once(IO.singleton_class, IOHook)
77
+ registry.prepend_once(Process.singleton_class, ProcessHook)
78
+ registry.prepend_once(Open3.singleton_class, Open3Hook) if defined?(Open3)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberAudit
4
+ module Runtime
5
+ module Probes
6
+ module Synchronization
7
+ TRY_LOCK_MEASUREMENTS = lambda do |result|
8
+ acquired = result == true
9
+ { acquired: acquired, contention_observed: !acquired }
10
+ end
11
+
12
+ module MutexHook
13
+ def lock(...)
14
+ Registry.observe(operation: 'Mutex#lock') { super }
15
+ end
16
+
17
+ def synchronize(...)
18
+ Registry.observe(operation: 'Mutex#synchronize') { super }
19
+ end
20
+
21
+ def try_lock(...)
22
+ Registry.observe(
23
+ operation: 'Mutex#try_lock',
24
+ measurement_builder: TRY_LOCK_MEASUREMENTS
25
+ ) { super }
26
+ end
27
+ end
28
+
29
+ module ConditionVariableHook
30
+ def wait(...)
31
+ Registry.observe(operation: 'ConditionVariable#wait') { super }
32
+ end
33
+ end
34
+
35
+ module MonitorHook
36
+ def synchronize(...)
37
+ Registry.observe(operation: 'Monitor#synchronize') { super }
38
+ end
39
+ end
40
+
41
+ module MonitorMixinHook
42
+ def synchronize(...)
43
+ Registry.observe(operation: 'MonitorMixin#synchronize') { super }
44
+ end
45
+ end
46
+
47
+ module_function
48
+
49
+ def install!(registry)
50
+ registry.prepend_once(Mutex, MutexHook)
51
+ registry.prepend_once(ConditionVariable, ConditionVariableHook) if defined?(ConditionVariable)
52
+ registry.prepend_once(Monitor, MonitorHook) if defined?(Monitor)
53
+ registry.prepend_once(MonitorMixin, MonitorMixinHook) if defined?(MonitorMixin)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberAudit
4
+ module Runtime
5
+ module Probes
6
+ module ThreadState
7
+ module Hook
8
+ def thread_variable_get(...)
9
+ Registry.observe(operation: 'Thread.thread_variable_get') { super }
10
+ end
11
+
12
+ def thread_variable_set(...)
13
+ Registry.observe(operation: 'Thread.thread_variable_set') { super }
14
+ end
15
+
16
+ def [](...)
17
+ return super unless equal?(Thread.current)
18
+
19
+ Registry.observe(operation: 'Thread.current.[]') { super }
20
+ end
21
+
22
+ def []=(...)
23
+ if equal?(Thread.current)
24
+ Registry.observe(operation: 'Thread.current.[]=') { super }
25
+ else
26
+ super
27
+ end
28
+ end
29
+ end
30
+
31
+ module_function
32
+
33
+ def install!(registry)
34
+ registry.prepend_once(Thread, Hook)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FiberAudit
4
+ module Runtime
5
+ module Probes
6
+ module ThreadWait
7
+ module Hook
8
+ def join(...)
9
+ Registry.observe(operation: 'Thread.join') { super }
10
+ end
11
+
12
+ def value(...)
13
+ Registry.observe(operation: 'Thread.value') { super }
14
+ end
15
+ end
16
+
17
+ module_function
18
+
19
+ def install!(registry)
20
+ registry.prepend_once(Thread, Hook)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'execution_context'
4
+
5
+ module FiberAudit
6
+ module Runtime
7
+ # Process-local, idempotent Rails integration.
8
+ # Installs prepend hooks for Rails boundaries when detected.
9
+ # Becomes inert after deactivation or fork.
10
+ # Supports late-load rescanning for Rails components loaded after boot.
11
+ class RailsIntegration
12
+ attr_reader :owner_pid, :context_store, :recorder
13
+
14
+ def initialize(context_store:, recorder: nil)
15
+ @context_store = context_store
16
+ @recorder = recorder
17
+ @owner_pid = Process.pid
18
+ @active = true
19
+ @middleware_stack = nil
20
+ @require_hook_installed = false
21
+ @mutex = Mutex.new
22
+ end
23
+
24
+ def active_for_current_process?
25
+ @active && @owner_pid == Process.pid
26
+ end
27
+
28
+ def deactivate!
29
+ @active = false
30
+ self
31
+ end
32
+
33
+ def install!
34
+ return unless active_for_current_process?
35
+
36
+ @mutex.synchronize do
37
+ install_require_hook
38
+ install_controller_hook
39
+ install_job_hook
40
+ install_cable_hook
41
+ try_install_middleware_hook
42
+ end
43
+ self
44
+ rescue StandardError => e
45
+ handle_installation_failure(e)
46
+ self
47
+ end
48
+
49
+ def rescan!
50
+ return unless active_for_current_process?
51
+
52
+ @mutex.synchronize do
53
+ install_controller_hook
54
+ install_job_hook
55
+ install_cable_hook
56
+ try_install_middleware_hook
57
+ end
58
+ self
59
+ rescue StandardError => e
60
+ handle_installation_failure(e)
61
+ self
62
+ end
63
+
64
+ class << self
65
+ def activate(context_store: ExecutionContext, recorder: nil)
66
+ integration = new(context_store: context_store, recorder: recorder)
67
+ integration.install!
68
+ @current = integration
69
+ @current
70
+ rescue StandardError
71
+ @current = nil
72
+ raise
73
+ end
74
+
75
+ attr_reader :current
76
+
77
+ def deactivate(integration = nil)
78
+ target = integration || @current
79
+ return unless target
80
+
81
+ target.deactivate!
82
+ @current = nil if @current.equal?(target)
83
+ end
84
+
85
+ def rescan!
86
+ @current&.rescan! if @current&.active_for_current_process?
87
+ end
88
+
89
+ def rescan_after_require!
90
+ key = :fiber_audit_rails_require_rescan
91
+ thread = Thread.current
92
+ acquired = false
93
+ return if thread.thread_variable_get(key)
94
+
95
+ thread.thread_variable_set(key, true)
96
+ acquired = true
97
+ rescan!
98
+ ensure
99
+ thread&.thread_variable_set(key, false) if acquired
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def handle_installation_failure(error)
106
+ unless error.instance_variable_defined?(:@fiber_audit_runtime_accounted)
107
+ recorder&.internal_error! unless recorder&.disabled?
108
+ error.instance_variable_set(:@fiber_audit_runtime_accounted, true)
109
+ end
110
+ raise error unless fail_open?
111
+ end
112
+
113
+ def fail_open?
114
+ return true unless recorder
115
+
116
+ recorder.session.policy.fail_open?
117
+ end
118
+
119
+ def account_internal_error
120
+ return unless recorder
121
+
122
+ recorder.internal_error! unless recorder.disabled?
123
+ end
124
+
125
+ def install_require_hook
126
+ return if @require_hook_installed
127
+
128
+ # Zeitwerk replaces Kernel#require. Load its optional integration before
129
+ # prepending FiberAudit so later Rails autoloads retain Zeitwerk's require
130
+ # semantics. This remains a no-op when Zeitwerk is not installed.
131
+ begin
132
+ require 'zeitwerk'
133
+ rescue LoadError
134
+ nil
135
+ end
136
+
137
+ # Install narrow guarded require hook for late Rails loading
138
+ # Independent of probe registry
139
+ ::Kernel.prepend(RequireHook) unless ::Kernel.ancestors.include?(RequireHook)
140
+ @require_hook_installed = true
141
+ end
142
+
143
+ def install_controller_hook
144
+ namespace = loaded_constant(Object, :ActionController)
145
+ target = loaded_constant(namespace, :Metal)
146
+ return unless target
147
+
148
+ target.prepend(ControllerHook) unless target.ancestors.include?(ControllerHook)
149
+ end
150
+
151
+ def install_job_hook
152
+ namespace = loaded_constant(Object, :ActiveJob)
153
+ target = loaded_constant(namespace, :Base)
154
+ return unless target
155
+
156
+ target.prepend(JobHook) unless target.ancestors.include?(JobHook)
157
+ end
158
+
159
+ def install_cable_hook
160
+ namespace = loaded_constant(Object, :ActionCable)
161
+ channel = loaded_constant(namespace, :Channel)
162
+ target = loaded_constant(channel, :Base)
163
+ return unless target
164
+
165
+ target.prepend(CableHook) unless target.ancestors.include?(CableHook)
166
+ end
167
+
168
+ def loaded_constant(namespace, name)
169
+ return unless namespace.is_a?(Module)
170
+ return if namespace.autoload?(name)
171
+ return unless namespace.const_defined?(name, false)
172
+
173
+ namespace.const_get(name, false)
174
+ rescue NameError
175
+ nil
176
+ end
177
+
178
+ def try_install_middleware_hook
179
+ rails = loaded_constant(Object, :Rails)
180
+ return unless rails
181
+
182
+ # Rails.application may instantiate the application class. Calling it
183
+ # from a require hook while Rails itself is loading can re-enter a
184
+ # partially initialized application, so only inspect an existing instance.
185
+ application = rails.instance_variable_get(:@application)
186
+ return unless application
187
+
188
+ stack = application.config.middleware
189
+ return if @middleware_stack.equal?(stack) || middleware_installed_in?(stack)
190
+
191
+ # Try to insert middleware into Rails stack. A replacement stack (for
192
+ # example after a Rails reload) is eligible for installation again.
193
+ begin
194
+ stack.insert_before(0, Middleware)
195
+ @middleware_stack = stack
196
+ rescue StandardError => e
197
+ # Stack may be finalized - that's okay, middleware is optional
198
+ handle_installation_failure(e)
199
+ end
200
+ end
201
+
202
+ def middleware_installed_in?(stack)
203
+ return false unless stack.respond_to?(:any?)
204
+
205
+ stack.any? do |entry|
206
+ entry.equal?(Middleware) || (entry.respond_to?(:klass) && entry.klass.equal?(Middleware))
207
+ end
208
+ end
209
+
210
+ # Require hook for late Rails loading - independent of probe registry
211
+ # Narrow, guarded, behavior-preserving
212
+ module RequireHook
213
+ def require(path)
214
+ result = super
215
+ # Only rescan after a successful require that loaded a feature.
216
+ RailsIntegration.rescan_after_require! if result
217
+ result
218
+ end
219
+
220
+ private :require
221
+ end
222
+
223
+ # Prepend hooks - consult active integration before setting context
224
+ # Resilient to context store failures in fail-open mode
225
+ # Critical: must not catch application exceptions or invoke app twice
226
+ module ControllerHook
227
+ def process_action(...)
228
+ integration = RailsIntegration.current
229
+ return super unless integration&.active_for_current_process?
230
+
231
+ executed = false
232
+ begin
233
+ integration.context_store.with(:request) do
234
+ executed = true
235
+ super
236
+ end
237
+ rescue StandardError
238
+ raise if executed
239
+ # Context setup failed before application code ran
240
+ raise unless integration.send(:fail_open?)
241
+
242
+ integration.send(:account_internal_error)
243
+ super
244
+ end
245
+ end
246
+ end
247
+
248
+ module JobHook
249
+ def perform_now(...)
250
+ integration = RailsIntegration.current
251
+ return super unless integration&.active_for_current_process?
252
+
253
+ executed = false
254
+ begin
255
+ integration.context_store.with(:job) do
256
+ executed = true
257
+ super
258
+ end
259
+ rescue StandardError
260
+ raise if executed
261
+ # Context setup failed before application code ran
262
+ raise unless integration.send(:fail_open?)
263
+
264
+ integration.send(:account_internal_error)
265
+ super
266
+ end
267
+ end
268
+ end
269
+
270
+ module CableHook
271
+ def dispatch_action(...)
272
+ integration = RailsIntegration.current
273
+ return super unless integration&.active_for_current_process?
274
+
275
+ executed = false
276
+ begin
277
+ integration.context_store.with(:websocket) do
278
+ executed = true
279
+ super
280
+ end
281
+ rescue StandardError
282
+ raise if executed
283
+ # Context setup failed before application code ran
284
+ raise unless integration.send(:fail_open?)
285
+
286
+ integration.send(:account_internal_error)
287
+ super
288
+ end
289
+ end
290
+ end
291
+
292
+ # Rack middleware for :middleware context
293
+ class Middleware
294
+ def initialize(app)
295
+ @app = app
296
+ end
297
+
298
+ def call(env)
299
+ integration = RailsIntegration.current
300
+ return @app.call(env) unless integration&.active_for_current_process?
301
+
302
+ executed = false
303
+ begin
304
+ integration.context_store.with(:middleware) do
305
+ executed = true
306
+ @app.call(env)
307
+ end
308
+ rescue StandardError
309
+ raise if executed
310
+ # Context setup failed before application code ran
311
+ raise unless integration.send(:fail_open?)
312
+
313
+ integration.send(:account_internal_error)
314
+ @app.call(env)
315
+ end
316
+ end
317
+ end
318
+ end
319
+ end
320
+ end