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.
- checksums.yaml +4 -4
- data/.fiber-audit.example.yml +19 -0
- data/ARCHITECTURE.md +726 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +201 -0
- data/README.md +66 -8
- data/lib/fiber_audit/cli.rb +87 -2
- data/lib/fiber_audit/configuration.rb +106 -6
- data/lib/fiber_audit/errors.rb +2 -0
- data/lib/fiber_audit/operation_vocabulary.rb +42 -0
- data/lib/fiber_audit/reporters/text.rb +1 -1
- data/lib/fiber_audit/runtime/active_operations.rb +146 -0
- data/lib/fiber_audit/runtime/boot.rb +83 -0
- data/lib/fiber_audit/runtime/clock.rb +35 -0
- data/lib/fiber_audit/runtime/environment.rb +289 -0
- data/lib/fiber_audit/runtime/event.rb +86 -0
- data/lib/fiber_audit/runtime/execution_context.rb +89 -0
- data/lib/fiber_audit/runtime/heartbeat.rb +113 -0
- data/lib/fiber_audit/runtime/jsonl/schema.rb +312 -0
- data/lib/fiber_audit/runtime/jsonl/writer.rb +122 -0
- data/lib/fiber_audit/runtime/lifecycle.rb +343 -0
- data/lib/fiber_audit/runtime/limits.rb +102 -0
- data/lib/fiber_audit/runtime/location.rb +44 -0
- data/lib/fiber_audit/runtime/policy.rb +121 -0
- data/lib/fiber_audit/runtime/probes/base.rb +333 -0
- data/lib/fiber_audit/runtime/probes/http.rb +80 -0
- data/lib/fiber_audit/runtime/probes/io_select.rb +50 -0
- data/lib/fiber_audit/runtime/probes/registry.rb +156 -0
- data/lib/fiber_audit/runtime/probes/socket.rb +76 -0
- data/lib/fiber_audit/runtime/probes/subprocess.rb +83 -0
- data/lib/fiber_audit/runtime/probes/synchronization.rb +58 -0
- data/lib/fiber_audit/runtime/probes/thread_state.rb +39 -0
- data/lib/fiber_audit/runtime/probes/thread_wait.rb +25 -0
- data/lib/fiber_audit/runtime/rails_integration.rb +279 -0
- data/lib/fiber_audit/runtime/recorder.rb +333 -0
- data/lib/fiber_audit/runtime/redactor.rb +102 -0
- data/lib/fiber_audit/runtime/sampler.rb +26 -0
- data/lib/fiber_audit/runtime/scheduler_observer.rb +128 -0
- data/lib/fiber_audit/runtime/session.rb +112 -0
- data/lib/fiber_audit/runtime/supervisor.rb +114 -0
- data/lib/fiber_audit/runtime/validation.rb +68 -0
- data/lib/fiber_audit/runtime/watchdog.rb +479 -0
- data/lib/fiber_audit/runtime/watchdog_policy.rb +64 -0
- data/lib/fiber_audit/runtime.rb +37 -0
- data/lib/fiber_audit/static/rules/blocking_subprocess.rb +3 -8
- data/lib/fiber_audit/static/rules/direct_socket.rb +2 -3
- data/lib/fiber_audit/static/rules/io_select.rb +2 -4
- data/lib/fiber_audit/static/rules/net_http_in_request.rb +3 -5
- data/lib/fiber_audit/static/rules/synchronization.rb +2 -6
- data/lib/fiber_audit/static/rules/thread_current_state.rb +3 -2
- data/lib/fiber_audit/static/rules/thread_join.rb +3 -2
- data/lib/fiber_audit/version.rb +1 -1
- data/lib/fiber_audit.rb +1 -0
- metadata +38 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'validation'
|
|
4
|
+
|
|
5
|
+
module FiberAudit
|
|
6
|
+
module Runtime
|
|
7
|
+
Policy = Data.define(
|
|
8
|
+
:redaction,
|
|
9
|
+
:sampling_rate,
|
|
10
|
+
:max_events_per_second,
|
|
11
|
+
:max_events_per_session,
|
|
12
|
+
:max_record_bytes,
|
|
13
|
+
:max_session_bytes,
|
|
14
|
+
:fail_open
|
|
15
|
+
) do
|
|
16
|
+
def initialize(**values)
|
|
17
|
+
unknown = values.keys - Policy::DEFAULTS.keys
|
|
18
|
+
raise RuntimeContractError, "unknown runtime policy field: #{unknown.first}" unless unknown.empty?
|
|
19
|
+
|
|
20
|
+
fields = Policy::DEFAULTS.merge(values)
|
|
21
|
+
redaction = normalize_redaction(fields[:redaction])
|
|
22
|
+
sampling_rate = normalize_sampling_rate(fields[:sampling_rate])
|
|
23
|
+
limits = Policy::LIMITS.to_h do |name, range|
|
|
24
|
+
[name, normalize_limit(fields[name], name, range)]
|
|
25
|
+
end
|
|
26
|
+
unless limits[:max_session_bytes] >= limits[:max_record_bytes]
|
|
27
|
+
raise RuntimeContractError, 'max_session_bytes must be >= max_record_bytes'
|
|
28
|
+
end
|
|
29
|
+
raise RuntimeContractError, 'fail_open must be a Boolean' unless [true, false].include?(fields[:fail_open])
|
|
30
|
+
|
|
31
|
+
super(
|
|
32
|
+
redaction: redaction,
|
|
33
|
+
sampling_rate: sampling_rate,
|
|
34
|
+
**limits,
|
|
35
|
+
fail_open: fields[:fail_open]
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sample?(draw:)
|
|
40
|
+
value = Validation.finite_number(draw, 'draw').to_f
|
|
41
|
+
raise RuntimeContractError, 'draw must be in 0.0...1.0' unless value >= 0.0 && value < 1.0
|
|
42
|
+
|
|
43
|
+
value < sampling_rate
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def rate_allowed?(emitted_in_window:)
|
|
47
|
+
count_below_limit?(emitted_in_window, max_events_per_second, 'emitted_in_window')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def session_event_allowed?(emitted_events:)
|
|
51
|
+
count_below_limit?(emitted_events, max_events_per_session, 'emitted_events')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def record_size_allowed?(bytes:)
|
|
55
|
+
size_allowed?(bytes, max_record_bytes, 'bytes')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def session_bytes_allowed?(written_bytes:, next_record_bytes:)
|
|
59
|
+
written = Validation.integer(written_bytes, 'written_bytes')
|
|
60
|
+
following = Validation.integer(next_record_bytes, 'next_record_bytes')
|
|
61
|
+
written + following <= max_session_bytes
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fail_open?
|
|
65
|
+
fail_open
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def strict_redaction?
|
|
69
|
+
redaction == :strict
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def normalize_redaction(value)
|
|
75
|
+
normalized = value.is_a?(String) || value.is_a?(Symbol) ? value.to_sym : nil
|
|
76
|
+
return :strict if normalized == :strict
|
|
77
|
+
|
|
78
|
+
raise RuntimeContractError, 'redaction must be strict'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def normalize_sampling_rate(value)
|
|
82
|
+
rate = Validation.finite_number(value, 'sampling_rate').to_f
|
|
83
|
+
return rate if rate.between?(0.0, 1.0)
|
|
84
|
+
|
|
85
|
+
raise RuntimeContractError, 'sampling_rate must be between 0.0 and 1.0'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def normalize_limit(value, field, range)
|
|
89
|
+
unless value.is_a?(Integer) && range.cover?(value)
|
|
90
|
+
raise RuntimeContractError, "#{field} must be an Integer in #{range}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def count_below_limit?(value, limit, field)
|
|
97
|
+
Validation.integer(value, field) < limit
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def size_allowed?(value, limit, field)
|
|
101
|
+
Validation.integer(value, field) <= limit
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Policy.const_set(:DEFAULTS, {
|
|
106
|
+
redaction: :strict,
|
|
107
|
+
sampling_rate: 0.1,
|
|
108
|
+
max_events_per_second: 100,
|
|
109
|
+
max_events_per_session: 10_000,
|
|
110
|
+
max_record_bytes: 16_384,
|
|
111
|
+
max_session_bytes: 10_485_760,
|
|
112
|
+
fail_open: true
|
|
113
|
+
}.freeze)
|
|
114
|
+
Policy.const_set(:LIMITS, {
|
|
115
|
+
max_events_per_second: 1..10_000,
|
|
116
|
+
max_events_per_session: 1..1_000_000,
|
|
117
|
+
max_record_bytes: 1_024..1_048_576,
|
|
118
|
+
max_session_bytes: 4_096..1_073_741_824
|
|
119
|
+
}.freeze)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../active_operations'
|
|
4
|
+
require_relative '../clock'
|
|
5
|
+
require_relative '../event'
|
|
6
|
+
require_relative '../execution_context'
|
|
7
|
+
require_relative '../rails_integration'
|
|
8
|
+
require_relative '../recorder'
|
|
9
|
+
require_relative '../redactor'
|
|
10
|
+
|
|
11
|
+
module FiberAudit
|
|
12
|
+
module Runtime
|
|
13
|
+
module Probes
|
|
14
|
+
# Shared behavior-preserving observation boundary for targeted wrappers.
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
16
|
+
class Base
|
|
17
|
+
SOURCE = :targeted_probe
|
|
18
|
+
MAX_CALLSITE_FRAMES = 32
|
|
19
|
+
INTERNAL_PATH = File.expand_path('../..', __dir__).freeze
|
|
20
|
+
ORIGINAL_MUTEX_SYNCHRONIZE = Mutex.instance_method(:synchronize)
|
|
21
|
+
|
|
22
|
+
Observation = Data.define(
|
|
23
|
+
:operation,
|
|
24
|
+
:started_monotonic_ns,
|
|
25
|
+
:location,
|
|
26
|
+
:handle,
|
|
27
|
+
:thread_id,
|
|
28
|
+
:fiber_id,
|
|
29
|
+
:measurements,
|
|
30
|
+
:execution_context
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
attr_reader :recorder, :clock, :redactor, :active_operations, :owner_pid, :execution_context_store
|
|
34
|
+
|
|
35
|
+
def initialize(recorder:, clock:, redactor:, active_operations:, execution_context_store: nil,
|
|
36
|
+
pid_source: Process.method(:pid))
|
|
37
|
+
validate_dependencies!(recorder, clock, redactor, active_operations, execution_context_store, pid_source)
|
|
38
|
+
@recorder = recorder
|
|
39
|
+
@clock = clock
|
|
40
|
+
@redactor = redactor
|
|
41
|
+
@active_operations = active_operations
|
|
42
|
+
@execution_context_store = execution_context_store
|
|
43
|
+
@pid_source = pid_source
|
|
44
|
+
@owner_pid = current_pid
|
|
45
|
+
@active = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def observe(operation:, measurements: {}, emit_start: false, measurement_builder: nil)
|
|
49
|
+
return yield unless active_for_current_process?
|
|
50
|
+
return yield if guarded?
|
|
51
|
+
|
|
52
|
+
observation = begin
|
|
53
|
+
prepare_observation(operation, measurements)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
instrumentation_failure(e)
|
|
56
|
+
end
|
|
57
|
+
return yield unless observation
|
|
58
|
+
|
|
59
|
+
with_guard { emit_start_observation(observation) } if emit_start
|
|
60
|
+
completed = false
|
|
61
|
+
result = nil
|
|
62
|
+
begin
|
|
63
|
+
result = yield
|
|
64
|
+
completed = true
|
|
65
|
+
result
|
|
66
|
+
ensure
|
|
67
|
+
finalize_observation(observation, completed: completed, result: result, measurement_builder: measurement_builder)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def with_guard
|
|
72
|
+
self.class.enter_guard
|
|
73
|
+
yield
|
|
74
|
+
ensure
|
|
75
|
+
self.class.exit_guard
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def deactivate
|
|
79
|
+
@active = false
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def active_for_current_process?
|
|
84
|
+
@active && owner_pid == current_pid
|
|
85
|
+
rescue StandardError
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def fail_open?
|
|
90
|
+
recorder.session.policy.fail_open?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def instrumentation_failure(error)
|
|
94
|
+
account_internal_error
|
|
95
|
+
raise error unless fail_open?
|
|
96
|
+
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def validate_dependencies!(candidate_recorder, candidate_clock, candidate_redactor, operations, context_store, pids)
|
|
103
|
+
raise RuntimeContractError, 'recorder must be a Runtime::Recorder' unless candidate_recorder.is_a?(Recorder)
|
|
104
|
+
raise RuntimeContractError, 'clock must be a Runtime::Clock' unless candidate_clock.is_a?(Clock)
|
|
105
|
+
raise RuntimeContractError, 'redactor must be a Runtime::Redactor' unless candidate_redactor.is_a?(Redactor)
|
|
106
|
+
unless operations.is_a?(ActiveOperations)
|
|
107
|
+
raise RuntimeContractError, 'active_operations must be Runtime::ActiveOperations'
|
|
108
|
+
end
|
|
109
|
+
if context_store && !context_store.respond_to?(:current)
|
|
110
|
+
raise RuntimeContractError, 'execution_context_store must respond to current'
|
|
111
|
+
end
|
|
112
|
+
raise RuntimeContractError, 'pid_source must respond to call' unless pids.respond_to?(:call)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def prepare_observation(operation, measurements)
|
|
116
|
+
# The block keeps all preparation under one recursion guard.
|
|
117
|
+
# rubocop:disable Metrics/BlockLength
|
|
118
|
+
with_guard do
|
|
119
|
+
canonical_operation = Validation.operation(operation)
|
|
120
|
+
normalized_measurements = normalize_measurements(measurements)
|
|
121
|
+
started_ns = clock.monotonic_ns
|
|
122
|
+
location = project_callsite
|
|
123
|
+
next unless location
|
|
124
|
+
|
|
125
|
+
thread = Thread.current
|
|
126
|
+
fiber = Fiber.current
|
|
127
|
+
captured_context = capture_execution_context
|
|
128
|
+
handle = active_operations.register(
|
|
129
|
+
operation: canonical_operation,
|
|
130
|
+
monotonic_ns: started_ns,
|
|
131
|
+
location: location,
|
|
132
|
+
execution_context: captured_context,
|
|
133
|
+
thread: thread,
|
|
134
|
+
fiber: fiber
|
|
135
|
+
)
|
|
136
|
+
Observation.new(
|
|
137
|
+
operation: canonical_operation,
|
|
138
|
+
started_monotonic_ns: started_ns,
|
|
139
|
+
location: location,
|
|
140
|
+
handle: handle,
|
|
141
|
+
thread_id: thread.object_id,
|
|
142
|
+
fiber_id: fiber.object_id,
|
|
143
|
+
measurements: normalized_measurements,
|
|
144
|
+
execution_context: captured_context
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
# rubocop:enable Metrics/BlockLength
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def capture_execution_context
|
|
151
|
+
return Context::UNKNOWN unless execution_context_store
|
|
152
|
+
|
|
153
|
+
execution_context_store.current
|
|
154
|
+
rescue StandardError
|
|
155
|
+
Context::UNKNOWN
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def emit_start_observation(observation)
|
|
159
|
+
emit_observation(:operation_started, observation, monotonic_ns: observation.started_monotonic_ns)
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
with_guard { active_operations.finish(observation.handle) } if observation.handle && !fail_open?
|
|
162
|
+
instrumentation_failure(e)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def finalize_observation(observation, completed:, result:, measurement_builder:)
|
|
166
|
+
error = nil
|
|
167
|
+
begin
|
|
168
|
+
if active_for_current_process?
|
|
169
|
+
with_guard do
|
|
170
|
+
ended_ns = clock.monotonic_ns
|
|
171
|
+
if ended_ns < observation.started_monotonic_ns
|
|
172
|
+
raise RuntimeSafetyError, 'probe monotonic clock moved backwards'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
measurements = completed_measurements(observation, completed, result, measurement_builder)
|
|
176
|
+
emit_observation(
|
|
177
|
+
completed ? :operation_completed : :operation_aborted,
|
|
178
|
+
observation,
|
|
179
|
+
monotonic_ns: ended_ns,
|
|
180
|
+
duration_ns: ended_ns - observation.started_monotonic_ns,
|
|
181
|
+
measurements: measurements
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
rescue StandardError => e
|
|
186
|
+
error = e
|
|
187
|
+
ensure
|
|
188
|
+
begin
|
|
189
|
+
with_guard { active_operations.finish(observation.handle) } if observation.handle
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
error ||= e
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
return unless error
|
|
196
|
+
|
|
197
|
+
account_internal_error
|
|
198
|
+
raise error if completed && !fail_open?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def completed_measurements(observation, completed, result, builder)
|
|
202
|
+
values = observation.measurements.dup
|
|
203
|
+
if completed && builder
|
|
204
|
+
generated = builder.call(result)
|
|
205
|
+
raise RuntimeContractError, 'measurement_builder must return a Hash' unless generated.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
values.merge!(generated)
|
|
208
|
+
end
|
|
209
|
+
values[:operation_sequence] = observation.handle&.sequence
|
|
210
|
+
values
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def emit_observation(kind, observation, monotonic_ns:, duration_ns: nil, measurements: nil)
|
|
214
|
+
values = measurements || observation.measurements.merge(operation_sequence: observation.handle&.sequence)
|
|
215
|
+
recorder.record do
|
|
216
|
+
Event.new(
|
|
217
|
+
kind: kind,
|
|
218
|
+
source: SOURCE,
|
|
219
|
+
occurred_at: clock.wall_time,
|
|
220
|
+
monotonic_ns: monotonic_ns,
|
|
221
|
+
duration_ns: duration_ns,
|
|
222
|
+
operation: observation.operation,
|
|
223
|
+
location: observation.location,
|
|
224
|
+
execution_context: observation.execution_context,
|
|
225
|
+
thread_id: observation.thread_id,
|
|
226
|
+
fiber_id: observation.fiber_id,
|
|
227
|
+
measurements: values
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def normalize_measurements(value)
|
|
233
|
+
raise RuntimeContractError, 'probe measurements must be a Hash' unless value.is_a?(Hash)
|
|
234
|
+
|
|
235
|
+
value.dup.freeze
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def project_callsite
|
|
239
|
+
caller_locations(0, MAX_CALLSITE_FRAMES)&.each do |frame|
|
|
240
|
+
paths = frame_paths(frame)
|
|
241
|
+
next if paths.any? { |path| internal_path?(path) }
|
|
242
|
+
|
|
243
|
+
return paths.filter_map { |path| safe_location(frame, path) }.first
|
|
244
|
+
rescue StandardError
|
|
245
|
+
return nil
|
|
246
|
+
end
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def frame_paths(frame)
|
|
251
|
+
candidates = [frame.absolute_path, frame.path].compact.filter_map do |path|
|
|
252
|
+
next unless path.is_a?(String) && !path.start_with?('-', '<')
|
|
253
|
+
|
|
254
|
+
File.absolute_path?(path) ? File.expand_path(path) : File.expand_path(path, redactor.root)
|
|
255
|
+
end
|
|
256
|
+
candidates.uniq
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def internal_path?(path)
|
|
260
|
+
path == INTERNAL_PATH || path.start_with?("#{INTERNAL_PATH}#{File::SEPARATOR}")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def safe_location(frame, path)
|
|
264
|
+
location = redactor.location(path: path, line: frame.lineno, column: nil)
|
|
265
|
+
return if Location::SENTINELS.include?(location.path)
|
|
266
|
+
|
|
267
|
+
location
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
class << self
|
|
271
|
+
def guarded?
|
|
272
|
+
state = guard_state
|
|
273
|
+
key = guard_key
|
|
274
|
+
ORIGINAL_MUTEX_SYNCHRONIZE.bind_call(state.fetch(:mutex)) do
|
|
275
|
+
state.fetch(:depths).fetch(key, 0).positive?
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def enter_guard
|
|
280
|
+
state = guard_state
|
|
281
|
+
key = guard_key
|
|
282
|
+
ORIGINAL_MUTEX_SYNCHRONIZE.bind_call(state.fetch(:mutex)) do
|
|
283
|
+
depths = state.fetch(:depths)
|
|
284
|
+
depths[key] = depths.fetch(key, 0) + 1
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def exit_guard
|
|
289
|
+
state = guard_state
|
|
290
|
+
key = guard_key
|
|
291
|
+
ORIGINAL_MUTEX_SYNCHRONIZE.bind_call(state.fetch(:mutex)) do
|
|
292
|
+
depths = state.fetch(:depths)
|
|
293
|
+
depth = depths.fetch(key, 1) - 1
|
|
294
|
+
depth.positive? ? depths[key] = depth : depths.delete(key)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
private
|
|
299
|
+
|
|
300
|
+
def guard_state
|
|
301
|
+
pid = Process.pid
|
|
302
|
+
return @guard_state if @guard_pid == pid && @guard_state
|
|
303
|
+
|
|
304
|
+
@guard_pid = pid
|
|
305
|
+
@guard_state = { mutex: Mutex.new, depths: {} }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def guard_key
|
|
309
|
+
[Thread.current.object_id, Fiber.current.object_id]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def guarded?
|
|
314
|
+
self.class.guarded?
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def account_internal_error
|
|
318
|
+
with_guard { recorder.internal_error! unless recorder.disabled? }
|
|
319
|
+
rescue StandardError
|
|
320
|
+
nil
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def current_pid
|
|
324
|
+
value = @pid_source.call
|
|
325
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
326
|
+
|
|
327
|
+
raise RuntimeContractError, 'pid_source must return a positive Integer'
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
# rubocop:enable Metrics/ClassLength
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Runtime
|
|
5
|
+
module Probes
|
|
6
|
+
module HTTP
|
|
7
|
+
HTTP_SCHEME = /\Ahttps?:/i
|
|
8
|
+
|
|
9
|
+
module NetHTTPClassHook
|
|
10
|
+
def get(...)
|
|
11
|
+
Registry.observe(operation: 'Net::HTTP.get') { super }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get_response(...)
|
|
15
|
+
Registry.observe(operation: 'Net::HTTP.get_response') { super }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def start(...)
|
|
19
|
+
Registry.observe(operation: 'Net::HTTP.start') { super }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module NetHTTPClassRequestHook
|
|
24
|
+
def request(...)
|
|
25
|
+
Registry.observe(operation: 'Net::HTTP.request') { super }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module NetHTTPInstanceHook
|
|
30
|
+
def start(...)
|
|
31
|
+
Registry.observe(operation: 'Net::HTTP.start') { super }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def request(...)
|
|
35
|
+
Registry.observe(operation: 'Net::HTTP.request') { super }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module URIHook
|
|
40
|
+
def open(*arguments, **, &)
|
|
41
|
+
return super unless HTTP.http_target?(arguments.first)
|
|
42
|
+
|
|
43
|
+
Registry.observe(operation: 'URI.open') { super }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module OpenURIHook
|
|
48
|
+
def open_uri(*arguments, **, &)
|
|
49
|
+
return super unless HTTP.http_target?(arguments.first)
|
|
50
|
+
|
|
51
|
+
Registry.observe(operation: 'OpenURI.open_uri') { super }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
module_function
|
|
56
|
+
|
|
57
|
+
def install!(registry)
|
|
58
|
+
if defined?(Net::HTTP)
|
|
59
|
+
registry.prepend_once(Net::HTTP.singleton_class, NetHTTPClassHook)
|
|
60
|
+
registry.prepend_once(Net::HTTP, NetHTTPInstanceHook)
|
|
61
|
+
registry.prepend_once(Net::HTTP.singleton_class, NetHTTPClassRequestHook) if Net::HTTP.respond_to?(:request)
|
|
62
|
+
end
|
|
63
|
+
registry.prepend_once(URI.singleton_class, URIHook) if defined?(URI) && URI.respond_to?(:open)
|
|
64
|
+
return unless defined?(OpenURI) && OpenURI.respond_to?(:open_uri)
|
|
65
|
+
|
|
66
|
+
registry.prepend_once(OpenURI.singleton_class, OpenURIHook)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def http_target?(value)
|
|
70
|
+
return value.match?(HTTP_SCHEME) if value.instance_of?(String) && value.valid_encoding?
|
|
71
|
+
return false unless defined?(URI::HTTP)
|
|
72
|
+
|
|
73
|
+
value.instance_of?(URI::HTTP) || (defined?(URI::HTTPS) && value.instance_of?(URI::HTTPS))
|
|
74
|
+
rescue StandardError
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FiberAudit
|
|
4
|
+
module Runtime
|
|
5
|
+
module Probes
|
|
6
|
+
module IOSelect
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def timeout_measurement(arguments)
|
|
10
|
+
{ timeout_present: arguments.length >= 4 && !arguments[3].nil? }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module IOHook
|
|
14
|
+
def select(*arguments, &)
|
|
15
|
+
Registry.observe(
|
|
16
|
+
operation: 'IO.select',
|
|
17
|
+
measurements: IOSelect.timeout_measurement(arguments)
|
|
18
|
+
) { super }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module KernelInstanceHook
|
|
23
|
+
def select(*arguments, &)
|
|
24
|
+
Registry.observe(
|
|
25
|
+
operation: 'Kernel.select',
|
|
26
|
+
measurements: IOSelect.timeout_measurement(arguments)
|
|
27
|
+
) { super }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private :select
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module KernelSingletonHook
|
|
34
|
+
def select(*arguments, &)
|
|
35
|
+
Registry.observe(
|
|
36
|
+
operation: 'Kernel.select',
|
|
37
|
+
measurements: IOSelect.timeout_measurement(arguments)
|
|
38
|
+
) { super }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def install!(registry)
|
|
43
|
+
registry.prepend_once(IO.singleton_class, IOHook)
|
|
44
|
+
registry.prepend_once(Kernel, KernelInstanceHook)
|
|
45
|
+
registry.prepend_once(Kernel.singleton_class, KernelSingletonHook)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|