fiber_audit 0.1.0 → 0.2.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.
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 +30 -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 +279 -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,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'http'
5
+ require_relative 'io_select'
6
+ require_relative 'socket'
7
+ require_relative 'subprocess'
8
+ require_relative 'synchronization'
9
+ require_relative 'thread_state'
10
+ require_relative 'thread_wait'
11
+
12
+ module FiberAudit
13
+ module Runtime
14
+ module Probes
15
+ # Process-local owner of idempotent probe installation and dispatch.
16
+ class Registry
17
+ PROBE_INSTALLERS = [
18
+ Subprocess,
19
+ ThreadWait,
20
+ Synchronization,
21
+ ThreadState,
22
+ IOSelect,
23
+ Socket,
24
+ HTTP
25
+ ].freeze
26
+
27
+ module RequireInstanceHook
28
+ def require(...)
29
+ result = super
30
+ Registry.rescan_current!
31
+ result
32
+ end
33
+
34
+ private :require
35
+ end
36
+
37
+ module RequireSingletonHook
38
+ def require(...)
39
+ result = super
40
+ Registry.rescan_current!
41
+ result
42
+ end
43
+ end
44
+
45
+ class << self
46
+ def activate(base:)
47
+ raise RuntimeContractError, 'base must be a Runtime::Probes::Base' unless base.is_a?(Base)
48
+
49
+ registry = new(base: base)
50
+ @current = registry
51
+ registry.install!
52
+ registry
53
+ rescue StandardError
54
+ registry&.deactivate
55
+ @current = nil if @current.equal?(registry)
56
+ raise
57
+ end
58
+
59
+ def observe(operation:, measurements: {}, emit_start: false, measurement_builder: nil, &application)
60
+ raise ArgumentError, 'probe observation requires a block' unless application
61
+
62
+ registry = current_for_observation
63
+ return application.call unless registry
64
+
65
+ registry.base.observe(
66
+ operation: operation,
67
+ measurements: measurements,
68
+ emit_start: emit_start,
69
+ measurement_builder: measurement_builder,
70
+ &application
71
+ )
72
+ end
73
+
74
+ def rescan_current!
75
+ current_for_observation&.scan!
76
+ end
77
+
78
+ def deactivate(registry)
79
+ @current = nil if @current.equal?(registry)
80
+ registry&.deactivate
81
+ end
82
+
83
+ def current
84
+ current_for_observation
85
+ end
86
+
87
+ private
88
+
89
+ def current_for_observation
90
+ registry = @current
91
+ return unless registry
92
+ return registry if registry.active_for_current_process?
93
+ return unless registry.stale_process?
94
+
95
+ FiberAudit::Runtime::Boot.current if defined?(FiberAudit::Runtime::Boot)
96
+ registry = @current
97
+ registry if registry&.active_for_current_process?
98
+ end
99
+ end
100
+
101
+ attr_reader :base, :owner_pid
102
+
103
+ def initialize(base:)
104
+ @base = base
105
+ @owner_pid = Process.pid
106
+ @mutex = Mutex.new
107
+ @active = true
108
+ end
109
+
110
+ def install!
111
+ scan!
112
+ self
113
+ end
114
+
115
+ def scan!
116
+ return self unless active_for_current_process?
117
+
118
+ base.with_guard do
119
+ @mutex.synchronize do
120
+ prepend_once(Kernel, RequireInstanceHook)
121
+ prepend_once(Kernel.singleton_class, RequireSingletonHook)
122
+ PROBE_INSTALLERS.each { |installer| installer.install!(self) }
123
+ end
124
+ end
125
+ self
126
+ rescue StandardError => e
127
+ base.instrumentation_failure(e)
128
+ self
129
+ end
130
+
131
+ def prepend_once(target, hook)
132
+ unless target.is_a?(Module) && hook.is_a?(Module)
133
+ raise RuntimeContractError, 'probe target and hook must be Modules'
134
+ end
135
+
136
+ target.prepend(hook) unless target.ancestors.include?(hook)
137
+ target
138
+ end
139
+
140
+ def deactivate
141
+ @active = false
142
+ base.deactivate
143
+ self
144
+ end
145
+
146
+ def active_for_current_process?
147
+ @active && owner_pid == Process.pid
148
+ end
149
+
150
+ def stale_process?
151
+ owner_pid != Process.pid
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../operation_vocabulary'
4
+
5
+ module FiberAudit
6
+ module Runtime
7
+ module Probes
8
+ module Socket
9
+ MODULE_NAME = Module.instance_method(:name)
10
+ MODULE_COMPARE = Module.instance_method(:<=)
11
+ CONST_DEFINED = Module.instance_method(:const_defined?)
12
+ CONST_GET = Module.instance_method(:const_get)
13
+
14
+ module ConstructorHook
15
+ def new(...)
16
+ operation = Socket.operation_for(self)
17
+ return super unless operation
18
+
19
+ Registry.observe(operation: operation) { super }
20
+ end
21
+ end
22
+
23
+ module_function
24
+
25
+ def install!(registry)
26
+ return unless defined?(::BasicSocket)
27
+
28
+ registry.prepend_once(::BasicSocket.singleton_class, ConstructorHook)
29
+ end
30
+
31
+ def operation_for(klass)
32
+ name = MODULE_NAME.bind_call(klass)
33
+ return unless safe_constant_name?(name)
34
+ return unless exact_socket?(klass, name) || ip_socket_subclass?(klass)
35
+ return unless resolve_constant(name).equal?(klass)
36
+
37
+ "#{name}.new"
38
+ rescue StandardError
39
+ nil
40
+ end
41
+
42
+ def exact_socket?(klass, name)
43
+ return false unless OperationVocabulary::FA1006_EXACT.include?(name)
44
+
45
+ resolve_constant(name).equal?(klass)
46
+ end
47
+
48
+ def ip_socket_subclass?(klass)
49
+ return false unless defined?(::IPSocket)
50
+
51
+ MODULE_COMPARE.bind_call(klass, ::IPSocket) == true
52
+ end
53
+
54
+ def safe_constant_name?(name)
55
+ name.is_a?(String) && name.bytesize <= 240 &&
56
+ name.match?(/\A[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*\z/)
57
+ end
58
+
59
+ def resolve_constant(name)
60
+ namespace = Object
61
+ name.split('::').each do |part|
62
+ unless CONST_DEFINED.bind_call(namespace, part, false)
63
+ namespace = nil
64
+ break
65
+ end
66
+
67
+ namespace = CONST_GET.bind_call(namespace, part, false)
68
+ end
69
+ namespace
70
+ rescue NameError, TypeError
71
+ nil
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -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