e11y 0.1.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 (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +69 -0
  4. data/CHANGELOG.md +26 -0
  5. data/CODE_OF_CONDUCT.md +64 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +179 -0
  8. data/Rakefile +37 -0
  9. data/benchmarks/run_all.rb +33 -0
  10. data/config/README.md +83 -0
  11. data/config/loki-local-config.yaml +35 -0
  12. data/config/prometheus.yml +15 -0
  13. data/docker-compose.yml +78 -0
  14. data/docs/00-ICP-AND-TIMELINE.md +483 -0
  15. data/docs/01-SCALE-REQUIREMENTS.md +858 -0
  16. data/docs/ADR-001-architecture.md +2617 -0
  17. data/docs/ADR-002-metrics-yabeda.md +1395 -0
  18. data/docs/ADR-003-slo-observability.md +3337 -0
  19. data/docs/ADR-004-adapter-architecture.md +2385 -0
  20. data/docs/ADR-005-tracing-context.md +1372 -0
  21. data/docs/ADR-006-security-compliance.md +4143 -0
  22. data/docs/ADR-007-opentelemetry-integration.md +1385 -0
  23. data/docs/ADR-008-rails-integration.md +1911 -0
  24. data/docs/ADR-009-cost-optimization.md +2993 -0
  25. data/docs/ADR-010-developer-experience.md +2166 -0
  26. data/docs/ADR-011-testing-strategy.md +1836 -0
  27. data/docs/ADR-012-event-evolution.md +958 -0
  28. data/docs/ADR-013-reliability-error-handling.md +2750 -0
  29. data/docs/ADR-014-event-driven-slo.md +1533 -0
  30. data/docs/ADR-015-middleware-order.md +1061 -0
  31. data/docs/ADR-016-self-monitoring-slo.md +1234 -0
  32. data/docs/API-REFERENCE-L28.md +914 -0
  33. data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
  34. data/docs/IMPLEMENTATION_NOTES.md +2804 -0
  35. data/docs/IMPLEMENTATION_PLAN.md +1971 -0
  36. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
  37. data/docs/PLAN.md +148 -0
  38. data/docs/QUICK-START.md +934 -0
  39. data/docs/README.md +296 -0
  40. data/docs/design/00-memory-optimization.md +593 -0
  41. data/docs/guides/MIGRATION-L27-L28.md +692 -0
  42. data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
  43. data/docs/guides/README.md +44 -0
  44. data/docs/prd/01-overview-vision.md +440 -0
  45. data/docs/use_cases/README.md +119 -0
  46. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
  47. data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
  48. data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
  49. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
  50. data/docs/use_cases/UC-005-sentry-integration.md +759 -0
  51. data/docs/use_cases/UC-006-trace-context-management.md +905 -0
  52. data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
  53. data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
  54. data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
  55. data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
  56. data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
  57. data/docs/use_cases/UC-012-audit-trail.md +2301 -0
  58. data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
  59. data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
  60. data/docs/use_cases/UC-015-cost-optimization.md +735 -0
  61. data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
  62. data/docs/use_cases/UC-017-local-development.md +867 -0
  63. data/docs/use_cases/UC-018-testing-events.md +1081 -0
  64. data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
  65. data/docs/use_cases/UC-020-event-versioning.md +708 -0
  66. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
  67. data/docs/use_cases/UC-022-event-registry.md +648 -0
  68. data/docs/use_cases/backlog.md +226 -0
  69. data/e11y.gemspec +76 -0
  70. data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
  71. data/lib/e11y/adapters/audit_encrypted.rb +239 -0
  72. data/lib/e11y/adapters/base.rb +580 -0
  73. data/lib/e11y/adapters/file.rb +224 -0
  74. data/lib/e11y/adapters/in_memory.rb +216 -0
  75. data/lib/e11y/adapters/loki.rb +333 -0
  76. data/lib/e11y/adapters/otel_logs.rb +203 -0
  77. data/lib/e11y/adapters/registry.rb +141 -0
  78. data/lib/e11y/adapters/sentry.rb +230 -0
  79. data/lib/e11y/adapters/stdout.rb +108 -0
  80. data/lib/e11y/adapters/yabeda.rb +370 -0
  81. data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
  82. data/lib/e11y/buffers/base_buffer.rb +40 -0
  83. data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
  84. data/lib/e11y/buffers/ring_buffer.rb +267 -0
  85. data/lib/e11y/buffers.rb +14 -0
  86. data/lib/e11y/console.rb +122 -0
  87. data/lib/e11y/current.rb +48 -0
  88. data/lib/e11y/event/base.rb +894 -0
  89. data/lib/e11y/event/value_sampling_config.rb +84 -0
  90. data/lib/e11y/events/base_audit_event.rb +43 -0
  91. data/lib/e11y/events/base_payment_event.rb +33 -0
  92. data/lib/e11y/events/rails/cache/delete.rb +21 -0
  93. data/lib/e11y/events/rails/cache/read.rb +23 -0
  94. data/lib/e11y/events/rails/cache/write.rb +22 -0
  95. data/lib/e11y/events/rails/database/query.rb +45 -0
  96. data/lib/e11y/events/rails/http/redirect.rb +21 -0
  97. data/lib/e11y/events/rails/http/request.rb +26 -0
  98. data/lib/e11y/events/rails/http/send_file.rb +21 -0
  99. data/lib/e11y/events/rails/http/start_processing.rb +26 -0
  100. data/lib/e11y/events/rails/job/completed.rb +22 -0
  101. data/lib/e11y/events/rails/job/enqueued.rb +22 -0
  102. data/lib/e11y/events/rails/job/failed.rb +22 -0
  103. data/lib/e11y/events/rails/job/scheduled.rb +23 -0
  104. data/lib/e11y/events/rails/job/started.rb +22 -0
  105. data/lib/e11y/events/rails/log.rb +56 -0
  106. data/lib/e11y/events/rails/view/render.rb +23 -0
  107. data/lib/e11y/events.rb +18 -0
  108. data/lib/e11y/instruments/active_job.rb +201 -0
  109. data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
  110. data/lib/e11y/instruments/sidekiq.rb +175 -0
  111. data/lib/e11y/logger/bridge.rb +205 -0
  112. data/lib/e11y/metrics/cardinality_protection.rb +172 -0
  113. data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
  114. data/lib/e11y/metrics/registry.rb +234 -0
  115. data/lib/e11y/metrics/relabeling.rb +226 -0
  116. data/lib/e11y/metrics.rb +102 -0
  117. data/lib/e11y/middleware/audit_signing.rb +174 -0
  118. data/lib/e11y/middleware/base.rb +140 -0
  119. data/lib/e11y/middleware/event_slo.rb +167 -0
  120. data/lib/e11y/middleware/pii_filter.rb +266 -0
  121. data/lib/e11y/middleware/pii_filtering.rb +280 -0
  122. data/lib/e11y/middleware/rate_limiting.rb +214 -0
  123. data/lib/e11y/middleware/request.rb +163 -0
  124. data/lib/e11y/middleware/routing.rb +157 -0
  125. data/lib/e11y/middleware/sampling.rb +254 -0
  126. data/lib/e11y/middleware/slo.rb +168 -0
  127. data/lib/e11y/middleware/trace_context.rb +131 -0
  128. data/lib/e11y/middleware/validation.rb +118 -0
  129. data/lib/e11y/middleware/versioning.rb +132 -0
  130. data/lib/e11y/middleware.rb +12 -0
  131. data/lib/e11y/pii/patterns.rb +90 -0
  132. data/lib/e11y/pii.rb +13 -0
  133. data/lib/e11y/pipeline/builder.rb +155 -0
  134. data/lib/e11y/pipeline/zone_validator.rb +110 -0
  135. data/lib/e11y/pipeline.rb +12 -0
  136. data/lib/e11y/presets/audit_event.rb +65 -0
  137. data/lib/e11y/presets/debug_event.rb +34 -0
  138. data/lib/e11y/presets/high_value_event.rb +51 -0
  139. data/lib/e11y/presets.rb +19 -0
  140. data/lib/e11y/railtie.rb +138 -0
  141. data/lib/e11y/reliability/circuit_breaker.rb +216 -0
  142. data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
  143. data/lib/e11y/reliability/dlq/filter.rb +117 -0
  144. data/lib/e11y/reliability/retry_handler.rb +207 -0
  145. data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
  146. data/lib/e11y/sampling/error_spike_detector.rb +225 -0
  147. data/lib/e11y/sampling/load_monitor.rb +161 -0
  148. data/lib/e11y/sampling/stratified_tracker.rb +92 -0
  149. data/lib/e11y/sampling/value_extractor.rb +82 -0
  150. data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
  151. data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
  152. data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
  153. data/lib/e11y/slo/event_driven.rb +150 -0
  154. data/lib/e11y/slo/tracker.rb +119 -0
  155. data/lib/e11y/version.rb +9 -0
  156. data/lib/e11y.rb +283 -0
  157. metadata +452 -0
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module E11y
6
+ module Buffers
7
+ # Adaptive buffer with global memory tracking and backpressure
8
+ #
9
+ # Key features:
10
+ # - Global memory limit (100MB default)
11
+ # - Per-adapter buffering (Hash-based)
12
+ # - Event size estimation (±10% accuracy)
13
+ # - Early flush at 80% threshold
14
+ # - Backpressure strategies (:block, :drop)
15
+ # - Thread-safe flush operations
16
+ #
17
+ # Architecture (C20 Resolution):
18
+ # - Tracks memory usage across ALL buffers globally
19
+ # - Enforces strict memory limits to prevent OOM
20
+ # - Adaptive behavior: flush early when approaching limit
21
+ # - Backpressure: block or drop events when limit exceeded
22
+ #
23
+ # References:
24
+ # - ADR-001 §3.3.2 (Adaptive Buffer with Memory Limits)
25
+ # - UC-001 (Request-Scoped Debug Buffering)
26
+ # - UC-014 (Adaptive Sampling)
27
+ #
28
+ # @example Basic usage
29
+ # buffer = E11y::Buffers::AdaptiveBuffer.new
30
+ # buffer.add_event({ event_name: "test", payload: {...}, adapters: [:logs] })
31
+ # events = buffer.flush # Returns all events
32
+ #
33
+ # @example With memory limit
34
+ # buffer = E11y::Buffers::AdaptiveBuffer.new(memory_limit_mb: 50)
35
+ # buffer.add_event(event) # May trigger backpressure if limit exceeded
36
+ #
37
+ # @see ADR-001 §3.3.2
38
+ # rubocop:disable Metrics/ClassLength
39
+ class AdaptiveBuffer
40
+ # Default memory limit (100 MB)
41
+ DEFAULT_MEMORY_LIMIT_MB = 100
42
+
43
+ # Early flush threshold (80%)
44
+ EARLY_FLUSH_THRESHOLD = 0.8
45
+
46
+ # Available backpressure strategies
47
+ BACKPRESSURE_STRATEGIES = %i[block drop].freeze
48
+
49
+ # Default backpressure strategy
50
+ DEFAULT_BACKPRESSURE_STRATEGY = :drop
51
+
52
+ # Maximum block time for :block strategy (seconds)
53
+ DEFAULT_MAX_BLOCK_TIME = 1.0
54
+
55
+ attr_reader :memory_limit_bytes
56
+
57
+ # Initialize adaptive buffer
58
+ #
59
+ # @param memory_limit_mb [Integer] Memory limit in megabytes (default: 100)
60
+ # @param backpressure_strategy [Symbol] Strategy when limit exceeded (:block, :drop)
61
+ # @param max_block_time [Float] Max wait time for :block strategy (seconds)
62
+ #
63
+ # @raise [ArgumentError] if memory_limit_mb <= 0 or invalid strategy
64
+ #
65
+ # @example
66
+ # buffer = AdaptiveBuffer.new(memory_limit_mb: 50, backpressure_strategy: :block)
67
+ def initialize(memory_limit_mb: DEFAULT_MEMORY_LIMIT_MB,
68
+ backpressure_strategy: DEFAULT_BACKPRESSURE_STRATEGY,
69
+ max_block_time: DEFAULT_MAX_BLOCK_TIME)
70
+ raise ArgumentError, "memory_limit_mb must be > 0" if memory_limit_mb <= 0
71
+
72
+ unless BACKPRESSURE_STRATEGIES.include?(backpressure_strategy)
73
+ raise ArgumentError,
74
+ "backpressure_strategy must be one of #{BACKPRESSURE_STRATEGIES.inspect}"
75
+ end
76
+
77
+ @memory_limit_bytes = memory_limit_mb * 1024 * 1024
78
+ @memory_warning_threshold = (@memory_limit_bytes * EARLY_FLUSH_THRESHOLD).to_i
79
+ @backpressure_strategy = backpressure_strategy
80
+ @max_block_time = max_block_time
81
+
82
+ # Per-adapter buffers (Hash: adapter_key => Array<event_hash>)
83
+ @buffers = {}
84
+
85
+ # Global memory tracking (atomic for thread-safety)
86
+ @total_memory_bytes = Concurrent::AtomicFixnum.new(0)
87
+
88
+ # Flush mutex (synchronize flush operations)
89
+ @flush_mutex = Mutex.new
90
+
91
+ # Early flush callback (optional, for integration with flush worker)
92
+ @early_flush_callback = nil
93
+ end
94
+
95
+ # Add event to buffer with memory tracking
96
+ #
97
+ # This is the main entry point for buffering events.
98
+ # Tracks memory usage, enforces limits, triggers early flush if needed.
99
+ #
100
+ # Behavior on memory limit exceeded:
101
+ # - :block - Waits up to max_block_time for space, then drops
102
+ # - :drop - Immediately drops event
103
+ #
104
+ # @param event_data [Hash] Event hash to buffer
105
+ # - Required keys: :event_name, :payload, :adapters (Array<Symbol>)
106
+ # @return [Boolean] true if event was added, false if dropped
107
+ #
108
+ # @example
109
+ # success = buffer.add_event({
110
+ # event_name: "UserSignup",
111
+ # payload: { user_id: 123 },
112
+ # adapters: [:logs, :errors_tracker]
113
+ # })
114
+ # # => true (or false if dropped due to memory limit)
115
+ def add_event(event_data)
116
+ event_size = estimate_size(event_data)
117
+ current_memory = @total_memory_bytes.value
118
+
119
+ # Check memory limit
120
+ return handle_memory_exhaustion(event_data, event_size) if current_memory + event_size > @memory_limit_bytes
121
+
122
+ # Add to appropriate adapter buffers
123
+ adapters = event_data[:adapters] || [:default]
124
+ adapters.each do |adapter_key|
125
+ @buffers[adapter_key] ||= []
126
+ @buffers[adapter_key] << event_data
127
+ end
128
+
129
+ # Update memory tracking atomically
130
+ @total_memory_bytes.update { |v| v + event_size }
131
+
132
+ # Warning threshold - trigger early flush (AFTER adding event to get accurate memory)
133
+ trigger_early_flush if @total_memory_bytes.value > @memory_warning_threshold
134
+
135
+ true
136
+ end
137
+
138
+ # Flush all buffers and return events
139
+ #
140
+ # Thread-safe operation (uses Mutex).
141
+ # Returns events grouped by adapter, updates memory tracking.
142
+ #
143
+ # @return [Hash] Events grouped by adapter key
144
+ # - Format: { adapter_key => [event1, event2, ...] }
145
+ #
146
+ # @example
147
+ # events = buffer.flush
148
+ # # => { logs: [...], errors_tracker: [...] }
149
+ def flush
150
+ @flush_mutex.synchronize do
151
+ events_by_adapter = {}
152
+ memory_freed = 0
153
+
154
+ @buffers.each do |adapter_key, events|
155
+ events_by_adapter[adapter_key] = events.dup
156
+
157
+ # Calculate memory freed
158
+ events.each { |event| memory_freed += estimate_size(event) }
159
+
160
+ # Clear buffer
161
+ events.clear
162
+ end
163
+
164
+ # Update memory tracking atomically
165
+ @total_memory_bytes.update { |v| [v - memory_freed, 0].max }
166
+
167
+ events_by_adapter
168
+ end
169
+ end
170
+
171
+ # Estimate memory size of event (C20 requirement: ±10% accuracy)
172
+ #
173
+ # Estimates memory footprint including:
174
+ # - Payload JSON size
175
+ # - Ruby object overhead (~200 bytes per Hash)
176
+ # - String overhead (~40 bytes per String key)
177
+ #
178
+ # @param event_data [Hash] Event hash
179
+ # @return [Integer] Estimated size in bytes
180
+ #
181
+ # @example
182
+ # size = buffer.estimate_size({ event_name: "test", payload: { id: 1 } })
183
+ # # => ~250 bytes (payload + overhead)
184
+ def estimate_size(event_data)
185
+ # Payload size (deep inspection of strings)
186
+ payload_size = calculate_payload_size(event_data[:payload])
187
+
188
+ # Ruby object overhead
189
+ base_overhead = 200 # Hash object (~200 bytes)
190
+ string_overhead = event_data.keys.size * 40 # String keys (~40 bytes each)
191
+
192
+ payload_size + base_overhead + string_overhead
193
+ end
194
+
195
+ # Get memory statistics for monitoring
196
+ #
197
+ # @return [Hash] Memory stats
198
+ # - :current_bytes - Current memory usage
199
+ # - :limit_bytes - Memory limit
200
+ # - :utilization - Percentage (0-100)
201
+ # - :buffer_counts - Events per adapter
202
+ #
203
+ # @example
204
+ # stats = buffer.memory_stats
205
+ # # => { current_bytes: 1024000, limit_bytes: 104857600, utilization: 0.98, ... }
206
+ def memory_stats
207
+ {
208
+ current_bytes: @total_memory_bytes.value,
209
+ limit_bytes: @memory_limit_bytes,
210
+ utilization: (@total_memory_bytes.value.to_f / @memory_limit_bytes * 100).round(2),
211
+ buffer_counts: @buffers.transform_values(&:size),
212
+ warning_threshold: @memory_warning_threshold
213
+ }
214
+ end
215
+
216
+ # Check if buffer is empty
217
+ #
218
+ # @return [Boolean] true if no events buffered
219
+ def empty?
220
+ @buffers.values.all?(&:empty?)
221
+ end
222
+
223
+ # Get total number of buffered events
224
+ #
225
+ # @return [Integer] Total events across all adapters
226
+ def size
227
+ @buffers.values.sum(&:size)
228
+ end
229
+
230
+ # Register early flush callback
231
+ #
232
+ # Called when buffer reaches 80% memory threshold.
233
+ # Used for integration with background flush worker.
234
+ #
235
+ # @param block [Proc] Callback to invoke on early flush
236
+ # @return [void]
237
+ #
238
+ # @example
239
+ # buffer.on_early_flush { FlushWorker.trigger_immediate_flush }
240
+ def on_early_flush(&block)
241
+ @early_flush_callback = block
242
+ end
243
+
244
+ private
245
+
246
+ # Calculate payload size recursively
247
+ #
248
+ # @param obj [Object] Payload object
249
+ # @return [Integer] Size in bytes
250
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
251
+ def calculate_payload_size(obj)
252
+ case obj
253
+ when String
254
+ obj.bytesize + 40 # String content + overhead
255
+ when Hash
256
+ obj.sum do |k, v|
257
+ k.to_s.bytesize + 40 + calculate_payload_size(v)
258
+ end
259
+ when Array
260
+ obj.sum { |v| calculate_payload_size(v) } + 40
261
+ when Numeric
262
+ 8 # Numbers are typically 8 bytes
263
+ else
264
+ 100 # Default for unknown types
265
+ end
266
+ rescue StandardError
267
+ 500 # Fallback for errors
268
+ end
269
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
270
+
271
+ # Handle memory exhaustion according to strategy
272
+ #
273
+ # @param event_data [Hash] Event that caused exhaustion
274
+ # @param event_size [Integer] Size of event
275
+ # @return [Boolean] true if event was eventually added, false if dropped
276
+ # rubocop:disable Metrics/MethodLength
277
+ def handle_memory_exhaustion(event_data, event_size)
278
+ case @backpressure_strategy
279
+ when :block
280
+ # Block event ingestion until space available
281
+ wait_start = Time.now
282
+
283
+ loop do
284
+ # Check if space available after flush
285
+ current_memory = @total_memory_bytes.value
286
+ break if current_memory + event_size <= @memory_limit_bytes
287
+
288
+ # Check timeout
289
+ if Time.now - wait_start > @max_block_time
290
+ # Timeout exceeded - drop event
291
+ increment_metric("e11y.buffer.memory_exhaustion.dropped")
292
+ return false
293
+ end
294
+
295
+ # Brief sleep to avoid busy-wait
296
+ sleep 0.01 # 10ms
297
+ end
298
+
299
+ # Space available - retry add
300
+ increment_metric("e11y.buffer.memory_exhaustion.blocked")
301
+ add_event(event_data)
302
+
303
+ when :drop
304
+ # Drop new event
305
+ increment_metric("e11y.buffer.memory_exhaustion.dropped")
306
+ false
307
+ end
308
+ end
309
+ # rubocop:enable Metrics/MethodLength
310
+
311
+ # Trigger early flush (80% threshold reached)
312
+ #
313
+ # Invokes registered callback if available.
314
+ # Used to notify flush worker to flush immediately.
315
+ #
316
+ # @return [void]
317
+ def trigger_early_flush
318
+ return unless @early_flush_callback
319
+
320
+ @early_flush_callback.call
321
+ rescue StandardError => e
322
+ # Silently ignore callback errors (don't break event tracking)
323
+ warn "E11y: Early flush callback failed: #{e.message}"
324
+ end
325
+
326
+ # Increment metric (placeholder for Phase 3: Metrics)
327
+ #
328
+ # TODO Phase 3: Replace with actual Yabeda metrics
329
+ #
330
+ # @param metric_name [String] Metric to increment
331
+ # @return [void]
332
+ def increment_metric(metric_name)
333
+ # Placeholder - will be implemented in Phase 3
334
+ # Yabeda.e11y.buffer_memory_exhaustion.increment(strategy: @backpressure_strategy)
335
+ end
336
+ # rubocop:enable Metrics/ClassLength
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Buffers
5
+ # Base class for all E11y buffers
6
+ #
7
+ # @abstract Subclass and implement buffer-specific logic
8
+ #
9
+ # @example Ring Buffer
10
+ # class RingBuffer < E11y::Buffers::BaseBuffer
11
+ # def initialize(capacity:)
12
+ # # Lock-free SPSC implementation
13
+ # end
14
+ # end
15
+ class BaseBuffer
16
+ # Push event to buffer
17
+ #
18
+ # @param event [Event::Base] event to buffer
19
+ # @return [Boolean] true if buffered, false if full
20
+ def push(_event)
21
+ raise NotImplementedError, "#{self.class}#push must be implemented"
22
+ end
23
+
24
+ # Flush all buffered events
25
+ #
26
+ # @yield [event] passes each event to block
27
+ # @return [Integer] number of flushed events
28
+ def flush
29
+ raise NotImplementedError, "#{self.class}#flush must be implemented"
30
+ end
31
+
32
+ # Get current buffer size
33
+ #
34
+ # @return [Integer] number of events in buffer
35
+ def size
36
+ raise NotImplementedError, "#{self.class}#size must be implemented"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Buffers
5
+ # Request-scoped buffer using thread-local storage for debug event buffering.
6
+ #
7
+ # This buffer stores debug events in thread-local storage during request processing.
8
+ # Events are flushed only when an error occurs, keeping logs clean during successful requests.
9
+ #
10
+ # Uses Thread.current for thread-local storage (compatible with any Ruby app, not just Rails).
11
+ # In Rails apps, this works seamlessly with Rack middleware thread model.
12
+ #
13
+ # @example Basic usage
14
+ # # In Rails middleware
15
+ # RequestScopedBuffer.initialize!
16
+ #
17
+ # # Track debug events (buffered)
18
+ # RequestScopedBuffer.add_event({ event_name: "debug", severity: :debug })
19
+ #
20
+ # # On error - flush all buffered events
21
+ # RequestScopedBuffer.flush_on_error
22
+ #
23
+ # # On success - discard buffered events
24
+ # RequestScopedBuffer.discard
25
+ #
26
+ # @see UC-001 Request-Scoped Debug Buffering
27
+ class RequestScopedBuffer
28
+ # Thread-local storage keys
29
+ THREAD_KEY_BUFFER = :e11y_request_buffer
30
+ THREAD_KEY_REQUEST_ID = :e11y_request_id
31
+ THREAD_KEY_ERROR_OCCURRED = :e11y_error_occurred
32
+ THREAD_KEY_BUFFER_LIMIT = :e11y_buffer_limit
33
+
34
+ # Default buffer limit per request
35
+ DEFAULT_BUFFER_LIMIT = 100
36
+
37
+ class << self
38
+ # Initialize request-scoped buffer
39
+ #
40
+ # @param request_id [String, nil] Optional request ID
41
+ # @param buffer_limit [Integer] Max events to buffer (default: 100)
42
+ # @return [void]
43
+ #
44
+ # @example
45
+ # RequestScopedBuffer.initialize!(request_id: "req-123", buffer_limit: 200)
46
+ def initialize!(request_id: nil, buffer_limit: DEFAULT_BUFFER_LIMIT)
47
+ Thread.current[THREAD_KEY_BUFFER] = []
48
+ Thread.current[THREAD_KEY_REQUEST_ID] = request_id || generate_request_id
49
+ Thread.current[THREAD_KEY_ERROR_OCCURRED] = false
50
+ Thread.current[THREAD_KEY_BUFFER_LIMIT] = buffer_limit
51
+ end
52
+
53
+ # Add event to request-scoped buffer
54
+ #
55
+ # Only buffers :debug severity events. Other severities return false.
56
+ # Triggers auto-flush if error severity detected.
57
+ #
58
+ # @param event_data [Hash] Event hash with :severity, :event_name, :payload
59
+ # @return [Boolean] true if buffered, false if not buffered
60
+ #
61
+ # @example
62
+ # # Debug event - buffered
63
+ # RequestScopedBuffer.add_event({ event_name: "test", severity: :debug })
64
+ # # => true
65
+ #
66
+ # # Error event - not buffered, triggers flush
67
+ # RequestScopedBuffer.add_event({ event_name: "error", severity: :error })
68
+ # # => false (and flushes buffer)
69
+ # rubocop:disable Metrics/MethodLength, Naming/PredicateMethod
70
+ def add_event(event_data)
71
+ return false unless active? # Not in request scope
72
+
73
+ severity = event_data[:severity]
74
+
75
+ # Trigger flush on error severity
76
+ if error_severity?(severity)
77
+ Thread.current[THREAD_KEY_ERROR_OCCURRED] = true
78
+ flush_on_error
79
+ return false # Error events not buffered
80
+ end
81
+
82
+ # Only buffer debug events
83
+ return false unless severity == :debug
84
+
85
+ current_buffer = buffer
86
+ return false if current_buffer.nil?
87
+
88
+ # Check buffer limit
89
+ if current_buffer.size >= buffer_limit
90
+ increment_metric("e11y.request_buffer.overflow")
91
+ return false # Buffer full, drop event
92
+ end
93
+
94
+ current_buffer << event_data
95
+ increment_metric("e11y.request_buffer.events_buffered")
96
+ true
97
+ end
98
+ # rubocop:enable Metrics/MethodLength, Naming/PredicateMethod
99
+
100
+ # Flush buffered events on error
101
+ #
102
+ # Sends all buffered debug events to the main buffer/adapters.
103
+ # Events keep their original :debug severity.
104
+ #
105
+ # @param target [Symbol, nil] Optional target adapter
106
+ # @return [Integer] Number of events flushed
107
+ #
108
+ # @example
109
+ # # In rescue block
110
+ # rescue StandardError => e
111
+ # RequestScopedBuffer.flush_on_error
112
+ # raise
113
+ # end
114
+ def flush_on_error(target: nil)
115
+ current_buffer = buffer
116
+ return 0 if current_buffer.nil? || current_buffer.empty?
117
+
118
+ flushed_count = current_buffer.size
119
+
120
+ # Flush events to main buffer/adapters
121
+ current_buffer.each do |event_data|
122
+ # TODO: Send to E11y::Collector.collect(event_data) when available
123
+ # For now, placeholder
124
+ flush_event(event_data, target: target)
125
+ end
126
+
127
+ current_buffer.clear
128
+ increment_metric("e11y.request_buffer.flushed_on_error", tags: { events: flushed_count })
129
+ flushed_count
130
+ end
131
+
132
+ # Discard buffered events (on successful request)
133
+ #
134
+ # @return [Integer] Number of events discarded
135
+ #
136
+ # @example
137
+ # # In middleware ensure block (success path)
138
+ # unless RequestScopedBuffer.error_occurred?
139
+ # RequestScopedBuffer.discard
140
+ # end
141
+ def discard
142
+ current_buffer = buffer
143
+ return 0 if current_buffer.nil? || current_buffer.empty?
144
+
145
+ discarded_count = current_buffer.size
146
+ current_buffer.clear
147
+ increment_metric("e11y.request_buffer.discarded", tags: { events: discarded_count })
148
+ discarded_count
149
+ end
150
+
151
+ # Check if request scope is active
152
+ #
153
+ # @return [Boolean] true if buffer initialized
154
+ def active?
155
+ !buffer.nil?
156
+ end
157
+
158
+ # Check if error occurred during request
159
+ #
160
+ # @return [Boolean] true if error severity detected
161
+ def error_occurred?
162
+ Thread.current[THREAD_KEY_ERROR_OCCURRED] == true
163
+ end
164
+
165
+ # Get current buffer size
166
+ #
167
+ # @return [Integer] Number of buffered events
168
+ def size
169
+ buffer&.size || 0
170
+ end
171
+
172
+ # Get current buffer
173
+ #
174
+ # @return [Array, nil] Buffer array or nil if not initialized
175
+ def buffer
176
+ Thread.current[THREAD_KEY_BUFFER]
177
+ end
178
+
179
+ # Get current request ID
180
+ #
181
+ # @return [String, nil] Request ID or nil if not initialized
182
+ def request_id
183
+ Thread.current[THREAD_KEY_REQUEST_ID]
184
+ end
185
+
186
+ # Reset request scope (cleanup)
187
+ #
188
+ # @return [void]
189
+ def reset_all
190
+ Thread.current[THREAD_KEY_BUFFER] = nil
191
+ Thread.current[THREAD_KEY_REQUEST_ID] = nil
192
+ Thread.current[THREAD_KEY_ERROR_OCCURRED] = nil
193
+ Thread.current[THREAD_KEY_BUFFER_LIMIT] = nil
194
+ end
195
+
196
+ private
197
+
198
+ # Get buffer limit (with fallback)
199
+ #
200
+ # @return [Integer] Buffer limit
201
+ def buffer_limit
202
+ Thread.current[THREAD_KEY_BUFFER_LIMIT] || DEFAULT_BUFFER_LIMIT
203
+ end
204
+
205
+ # Check if severity is error-level
206
+ #
207
+ # @param severity [Symbol] Event severity
208
+ # @return [Boolean] true if :error or :fatal
209
+ def error_severity?(severity)
210
+ %i[error fatal].include?(severity)
211
+ end
212
+
213
+ # Generate unique request ID
214
+ #
215
+ # @return [String] UUID request ID
216
+ def generate_request_id
217
+ require "securerandom"
218
+ SecureRandom.uuid
219
+ end
220
+
221
+ # Flush single event to adapters
222
+ #
223
+ # @param event_data [Hash] Event to flush
224
+ # @param target [Symbol, nil] Optional target adapter (not yet implemented)
225
+ # @return [void]
226
+ def flush_event(_event_data, target: nil) # rubocop:disable Lint/UnusedMethodArgument
227
+ # Placeholder for E11y::Collector integration
228
+ # Will be implemented when Collector/Adapter classes are available
229
+
230
+ # For now, just increment metric
231
+ increment_metric("e11y.request_buffer.event_flushed")
232
+ end
233
+
234
+ # Increment metric (placeholder)
235
+ #
236
+ # @param metric_name [String] Metric name
237
+ # @param tags [Hash] Optional tags
238
+ # @return [void]
239
+ def increment_metric(metric_name, tags: {})
240
+ # Placeholder for Yabeda integration
241
+ # Will be implemented in Phase 1 L2.4 (Metrics)
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end