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,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'clock'
5
+ require_relative 'active_operations'
6
+ require_relative 'environment'
7
+ require_relative 'execution_context'
8
+ require_relative 'rails_integration'
9
+ require_relative 'jsonl/writer'
10
+ require_relative 'recorder'
11
+ require_relative 'redactor'
12
+ require_relative 'probes/registry'
13
+ require_relative 'scheduler_observer'
14
+ require_relative 'session'
15
+ require_relative 'watchdog'
16
+ require_relative 'watchdog_policy'
17
+
18
+ module FiberAudit
19
+ module Runtime
20
+ # rubocop:disable Metrics/ClassLength
21
+ class Lifecycle
22
+ attr_reader :settings, :owner_pid, :output_path, :recorder,
23
+ :watchdog, :active_operations, :watchdog_policy,
24
+ :redactor, :probe_registry, :execution_context_store,
25
+ :rails_integration
26
+
27
+ def self.start(...)
28
+ new(...)
29
+ end
30
+
31
+ def initialize(
32
+ settings:,
33
+ watchdog_policy: nil,
34
+ probes_enabled: false,
35
+ clock: Clock.new,
36
+ session_id_source: SecureRandom.method(:uuid),
37
+ pid_source: Process.method(:pid),
38
+ writer_factory: JSONL::Writer.method(:open),
39
+ random: Sampler::RANDOM_SOURCE
40
+ )
41
+ validate_dependencies!(
42
+ settings, watchdog_policy, probes_enabled, clock,
43
+ session_id_source, pid_source, writer_factory, random
44
+ )
45
+ @settings = settings
46
+ @watchdog_policy = watchdog_policy
47
+ @probes_enabled = probes_enabled
48
+ @clock = clock
49
+ @session_id_source = session_id_source
50
+ @pid_source = pid_source
51
+ @writer_factory = writer_factory
52
+ @random = random
53
+ @owner_pid = current_pid
54
+ @state = :starting
55
+ @output_path = nil
56
+ @recorder = nil
57
+ @watchdog = nil
58
+ @scheduler_observer = nil
59
+ @active_operations = nil
60
+ @redactor = nil
61
+ @probe_registry = nil
62
+ @execution_context_store = nil
63
+ @rails_integration = nil
64
+ start_process_session!
65
+ rescue StandardError => e
66
+ startup_failure!(e)
67
+ end
68
+
69
+ def active?
70
+ @state == :active && recorder&.active?
71
+ end
72
+
73
+ def disabled?
74
+ @state == :disabled || recorder&.disabled?
75
+ end
76
+
77
+ def closed?
78
+ @state == :closed
79
+ end
80
+
81
+ def ensure_current_process!
82
+ pid = current_pid
83
+ return self if pid == owner_pid
84
+
85
+ abandon_inherited_runtime!
86
+ @owner_pid = pid
87
+ @output_path = nil
88
+ @recorder = nil
89
+ @watchdog = nil
90
+ @scheduler_observer = nil
91
+ @active_operations = nil
92
+ @redactor = nil
93
+ @probe_registry = nil
94
+ @execution_context_store = nil
95
+ @rails_integration = nil
96
+ @state = :starting
97
+ # Reset fiber-local context after fork
98
+ ExecutionContext.reset!
99
+ start_process_session!
100
+ self
101
+ rescue StandardError => e
102
+ process_failure!(e)
103
+ end
104
+
105
+ def shutdown(exception: nil)
106
+ ensure_current_process!
107
+ return @summary if closed?
108
+
109
+ runtime_error = stop_runtime_observers
110
+ status = runtime_error && exception.nil? ? :degraded : shutdown_status(exception)
111
+ @summary = recorder&.close(status: status)
112
+ @state = :closed
113
+ raise runtime_error if runtime_error && !settings.policy.fail_open?
114
+
115
+ @summary
116
+ rescue StandardError => e
117
+ @state = :closed
118
+ raise e unless settings.policy.fail_open?
119
+
120
+ nil
121
+ end
122
+
123
+ private
124
+
125
+ def validate_dependencies!(candidate_settings, watchdog, probes, candidate_clock, session_ids, pids, writers, random)
126
+ unless candidate_settings.is_a?(Environment::Settings)
127
+ raise RuntimeContractError, 'settings must be FiberAudit::Runtime::Environment::Settings'
128
+ end
129
+ unless watchdog.nil? || watchdog.is_a?(WatchdogPolicy)
130
+ raise RuntimeContractError, 'watchdog_policy must be a FiberAudit::Runtime::WatchdogPolicy or nil'
131
+ end
132
+ raise RuntimeContractError, 'probes_enabled must be a Boolean' unless [true, false].include?(probes)
133
+ raise RuntimeContractError, 'clock must be a FiberAudit::Runtime::Clock' unless candidate_clock.is_a?(Clock)
134
+
135
+ {
136
+ 'session_id_source' => session_ids,
137
+ 'pid_source' => pids,
138
+ 'writer_factory' => writers,
139
+ 'random source' => random
140
+ }.each do |name, source|
141
+ raise RuntimeContractError, "#{name} must respond to call" unless source.respond_to?(:call)
142
+ end
143
+ end
144
+
145
+ def start_process_session!
146
+ session_id = @session_id_source.call
147
+ started_at = @clock.wall_time
148
+ started_monotonic_ns = @clock.monotonic_ns
149
+ @output_path = build_output_path(session_id)
150
+ session = Session.new(
151
+ id: session_id,
152
+ started_at: started_at,
153
+ started_monotonic_ns: started_monotonic_ns,
154
+ policy: settings.policy
155
+ )
156
+ writer = @writer_factory.call(path: output_path, max_record_bytes: settings.policy.max_record_bytes)
157
+ @recorder = start_recorder(session, writer)
158
+ setup_runtime_observers! if recorder.active?
159
+ @state = recorder.active? ? :active : :disabled
160
+ end
161
+
162
+ def start_recorder(session, writer)
163
+ Recorder.start(session: session, writer: writer, clock: @clock, random: @random)
164
+ rescue StandardError
165
+ begin
166
+ writer.close
167
+ rescue StandardError
168
+ nil
169
+ end
170
+ raise
171
+ end
172
+
173
+ def setup_runtime_observers!
174
+ @active_operations = ActiveOperations.new(pid_source: @pid_source)
175
+ @redactor = Redactor.new(root: settings.project_root, policy: settings.policy)
176
+ @execution_context_store = ExecutionContext if @probes_enabled
177
+ setup_watchdog! if watchdog_policy
178
+ setup_rails_integration! if @probes_enabled
179
+ setup_probes! if @probes_enabled
180
+ rescue StandardError => e
181
+ unless e.instance_variable_defined?(:@fiber_audit_runtime_accounted)
182
+ recorder.internal_error!
183
+ e.instance_variable_set(:@fiber_audit_runtime_accounted, true)
184
+ end
185
+ deactivate_active_components!
186
+ close_failed_startup!(e)
187
+ end
188
+
189
+ def deactivate_active_components!
190
+ # Deactivate components in reverse order of setup
191
+ begin
192
+ Probes::Registry.deactivate(@probe_registry) if @probe_registry
193
+ rescue StandardError
194
+ nil
195
+ ensure
196
+ @probe_registry = nil
197
+ end
198
+ begin
199
+ RailsIntegration.deactivate(@rails_integration) if @rails_integration
200
+ rescue StandardError
201
+ nil
202
+ ensure
203
+ @rails_integration = nil
204
+ end
205
+ begin
206
+ if @scheduler_observer
207
+ SchedulerObserver.deactivate(@scheduler_observer)
208
+ @watchdog&.stop
209
+ end
210
+ rescue StandardError
211
+ nil
212
+ ensure
213
+ @scheduler_observer = nil
214
+ @watchdog = nil
215
+ end
216
+ end
217
+
218
+ def setup_rails_integration!
219
+ @rails_integration = RailsIntegration.activate(
220
+ context_store: @execution_context_store,
221
+ recorder: recorder
222
+ )
223
+ rescue StandardError
224
+ @rails_integration = nil
225
+ raise unless settings.policy.fail_open?
226
+ end
227
+
228
+ def setup_watchdog!
229
+ @watchdog = Watchdog.new(
230
+ policy: watchdog_policy,
231
+ recorder: recorder,
232
+ redactor: redactor,
233
+ active_operations: active_operations,
234
+ clock: @clock
235
+ )
236
+ @scheduler_observer = SchedulerObserver.activate(watchdog: watchdog) if watchdog.enabled?
237
+ end
238
+
239
+ def setup_probes!
240
+ base = Probes::Base.new(
241
+ recorder: recorder,
242
+ clock: @clock,
243
+ redactor: redactor,
244
+ active_operations: active_operations,
245
+ execution_context_store: @execution_context_store,
246
+ pid_source: @pid_source
247
+ )
248
+ @probe_registry = Probes::Registry.activate(base: base)
249
+ end
250
+
251
+ def close_failed_startup!(error)
252
+ @probe_registry = nil
253
+ return if settings.policy.fail_open?
254
+
255
+ begin
256
+ recorder.close(status: :degraded)
257
+ rescue StandardError
258
+ nil
259
+ end
260
+ raise error
261
+ end
262
+
263
+ def build_output_path(session_id)
264
+ unless session_id.is_a?(String) && session_id.match?(Validation::UUID)
265
+ raise RuntimeContractError, 'session ID source must return a canonical lowercase UUID'
266
+ end
267
+
268
+ filename = "fiber-audit-runtime-#{settings.launch_id}-#{owner_pid}-#{session_id}.jsonl"
269
+ File.join(settings.output_directory, filename).freeze
270
+ end
271
+
272
+ def abandon_inherited_runtime!
273
+ recorder&.writer&.close
274
+ ensure
275
+ @recorder = nil
276
+ @watchdog = nil
277
+ @scheduler_observer = nil
278
+ @active_operations = nil
279
+ @redactor = nil
280
+ @probe_registry = nil
281
+ @execution_context_store = nil
282
+ @rails_integration = nil
283
+ end
284
+
285
+ def stop_runtime_observers
286
+ error = nil
287
+ begin
288
+ RailsIntegration.deactivate(@rails_integration) if @rails_integration
289
+ rescue StandardError => e
290
+ error ||= e
291
+ ensure
292
+ @rails_integration = nil
293
+ end
294
+ begin
295
+ Probes::Registry.deactivate(probe_registry) if probe_registry
296
+ rescue StandardError => e
297
+ error ||= e
298
+ ensure
299
+ @probe_registry = nil
300
+ end
301
+ begin
302
+ SchedulerObserver.deactivate(@scheduler_observer) if @scheduler_observer
303
+ watchdog&.stop
304
+ rescue StandardError => e
305
+ error ||= e
306
+ ensure
307
+ @scheduler_observer = nil
308
+ end
309
+ recorder&.internal_error! if error
310
+ error
311
+ end
312
+
313
+ def startup_failure!(error)
314
+ raise error unless defined?(@settings) && settings.policy.fail_open?
315
+
316
+ @state = :disabled
317
+ end
318
+
319
+ def process_failure!(error)
320
+ raise error unless settings.policy.fail_open?
321
+
322
+ @state = :disabled
323
+ self
324
+ end
325
+
326
+ def current_pid
327
+ value = @pid_source.call
328
+ unless value.is_a?(Integer) && value.positive?
329
+ raise RuntimeContractError, 'pid source must return a positive Integer'
330
+ end
331
+
332
+ value
333
+ end
334
+
335
+ def shutdown_status(exception)
336
+ return :completed if exception.nil? || exception.is_a?(SystemExit)
337
+
338
+ :aborted
339
+ end
340
+ end
341
+ # rubocop:enable Metrics/ClassLength
342
+ end
343
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'policy'
4
+ require_relative 'session'
5
+ require_relative 'validation'
6
+
7
+ module FiberAudit
8
+ module Runtime
9
+ class Limits
10
+ WINDOW_NS = 1_000_000_000
11
+ MAX_COUNTER = 9_223_372_036_854_775_807
12
+ COUNTERS = SessionSummary::COUNTERS
13
+ DROP_REASONS = %i[
14
+ sampled_out rate_limited session_event_limited session_byte_limited oversize
15
+ ].freeze
16
+
17
+ attr_reader :policy, :started_monotonic_ns
18
+
19
+ def initialize(policy:, started_monotonic_ns:)
20
+ raise RuntimeContractError, 'policy must be a FiberAudit::Runtime::Policy' unless policy.is_a?(Policy)
21
+
22
+ @policy = policy
23
+ @started_monotonic_ns = Validation.integer(started_monotonic_ns, 'started_monotonic_ns')
24
+ @counters = COUNTERS.to_h { |name| [name, 0] }
25
+ @window_index = 0
26
+ @emitted_in_window = 0
27
+ @last_admission_ns = @started_monotonic_ns
28
+ end
29
+
30
+ def observe!
31
+ increment!(:events_observed)
32
+ end
33
+
34
+ def sampled_out!
35
+ increment!(:sampled_out)
36
+ end
37
+
38
+ def preflight_event(now_ns:)
39
+ now = normalize_admission_time(now_ns)
40
+ advance_window!(now)
41
+ return :session_event_limited unless policy.session_event_allowed?(emitted_events: @counters[:events_emitted])
42
+ return :rate_limited unless policy.rate_allowed?(emitted_in_window: @emitted_in_window)
43
+
44
+ nil
45
+ end
46
+
47
+ def drop!(reason)
48
+ raise RuntimeContractError, "unknown runtime drop reason: #{reason.inspect}" unless DROP_REASONS.include?(reason)
49
+
50
+ increment!(reason)
51
+ end
52
+
53
+ def emitted!(now_ns:)
54
+ now = normalize_admission_time(now_ns)
55
+ advance_window!(now)
56
+ increment!(:events_emitted)
57
+ increment_window!
58
+ end
59
+
60
+ def internal_error!(count: 1)
61
+ amount = Validation.integer(count, 'internal error count', minimum: 1)
62
+ increment!(:internal_errors, amount)
63
+ end
64
+
65
+ def counters
66
+ @counters.dup.freeze
67
+ end
68
+
69
+ private
70
+
71
+ def normalize_admission_time(value)
72
+ now = Validation.integer(value, 'admission monotonic time')
73
+ raise RuntimeContractError, 'admission monotonic time precedes the session' if now < @started_monotonic_ns
74
+ raise RuntimeSafetyError, 'monotonic clock moved backwards' if now < @last_admission_ns
75
+
76
+ @last_admission_ns = now
77
+ end
78
+
79
+ def advance_window!(now)
80
+ index = (now - started_monotonic_ns) / WINDOW_NS
81
+ return if index == @window_index
82
+
83
+ @window_index = index
84
+ @emitted_in_window = 0
85
+ end
86
+
87
+ def increment!(name, amount = 1)
88
+ following = @counters.fetch(name) + amount
89
+ raise RuntimeSafetyError, "runtime counter overflow: #{name}" if following > MAX_COUNTER
90
+
91
+ @counters[name] = following
92
+ end
93
+
94
+ def increment_window!
95
+ following = @emitted_in_window + 1
96
+ raise RuntimeSafetyError, 'runtime rate counter overflow' if following > MAX_COUNTER
97
+
98
+ @emitted_in_window = following
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require_relative 'validation'
5
+
6
+ module FiberAudit
7
+ module Runtime
8
+ Location = Data.define(:path, :line, :column) do
9
+ def initialize(path:, line: nil, column: nil)
10
+ super(
11
+ path: normalize_path(path),
12
+ line: Validation.integer(line, 'line', minimum: 1, allow_nil: true),
13
+ column: Validation.integer(column, 'column', allow_nil: true)
14
+ )
15
+ end
16
+
17
+ private
18
+
19
+ def normalize_path(value)
20
+ path = Validation.string(value, 'path', max_bytes: Location::MAX_PATH_BYTES)
21
+ return path if Location::SENTINELS.include?(path)
22
+
23
+ normalized = path.tr('\\', '/')
24
+ raise RuntimeContractError, 'path must be project-relative' if absolute_path?(normalized)
25
+
26
+ clean = Pathname.new(normalized).cleanpath.to_s
27
+ if clean == '.' || clean == '..' || clean.start_with?('../')
28
+ raise RuntimeContractError, 'path must not escape the project root'
29
+ end
30
+
31
+ clean.freeze
32
+ rescue ArgumentError
33
+ raise RuntimeContractError, 'path is invalid'
34
+ end
35
+
36
+ def absolute_path?(path)
37
+ path.start_with?('/', '//') || path.match?(%r{\A[A-Za-z]:/})
38
+ end
39
+ end
40
+
41
+ Location.const_set(:SENTINELS, %w[[external] [redacted]].freeze)
42
+ Location.const_set(:MAX_PATH_BYTES, 1_024)
43
+ end
44
+ end
@@ -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