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.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +69 -0
- data/CHANGELOG.md +26 -0
- data/CODE_OF_CONDUCT.md +64 -0
- data/LICENSE.txt +21 -0
- data/README.md +179 -0
- data/Rakefile +37 -0
- data/benchmarks/run_all.rb +33 -0
- data/config/README.md +83 -0
- data/config/loki-local-config.yaml +35 -0
- data/config/prometheus.yml +15 -0
- data/docker-compose.yml +78 -0
- data/docs/00-ICP-AND-TIMELINE.md +483 -0
- data/docs/01-SCALE-REQUIREMENTS.md +858 -0
- data/docs/ADR-001-architecture.md +2617 -0
- data/docs/ADR-002-metrics-yabeda.md +1395 -0
- data/docs/ADR-003-slo-observability.md +3337 -0
- data/docs/ADR-004-adapter-architecture.md +2385 -0
- data/docs/ADR-005-tracing-context.md +1372 -0
- data/docs/ADR-006-security-compliance.md +4143 -0
- data/docs/ADR-007-opentelemetry-integration.md +1385 -0
- data/docs/ADR-008-rails-integration.md +1911 -0
- data/docs/ADR-009-cost-optimization.md +2993 -0
- data/docs/ADR-010-developer-experience.md +2166 -0
- data/docs/ADR-011-testing-strategy.md +1836 -0
- data/docs/ADR-012-event-evolution.md +958 -0
- data/docs/ADR-013-reliability-error-handling.md +2750 -0
- data/docs/ADR-014-event-driven-slo.md +1533 -0
- data/docs/ADR-015-middleware-order.md +1061 -0
- data/docs/ADR-016-self-monitoring-slo.md +1234 -0
- data/docs/API-REFERENCE-L28.md +914 -0
- data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
- data/docs/IMPLEMENTATION_NOTES.md +2804 -0
- data/docs/IMPLEMENTATION_PLAN.md +1971 -0
- data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
- data/docs/PLAN.md +148 -0
- data/docs/QUICK-START.md +934 -0
- data/docs/README.md +296 -0
- data/docs/design/00-memory-optimization.md +593 -0
- data/docs/guides/MIGRATION-L27-L28.md +692 -0
- data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
- data/docs/guides/README.md +44 -0
- data/docs/prd/01-overview-vision.md +440 -0
- data/docs/use_cases/README.md +119 -0
- data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
- data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
- data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
- data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
- data/docs/use_cases/UC-005-sentry-integration.md +759 -0
- data/docs/use_cases/UC-006-trace-context-management.md +905 -0
- data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
- data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
- data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
- data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
- data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
- data/docs/use_cases/UC-012-audit-trail.md +2301 -0
- data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
- data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
- data/docs/use_cases/UC-015-cost-optimization.md +735 -0
- data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
- data/docs/use_cases/UC-017-local-development.md +867 -0
- data/docs/use_cases/UC-018-testing-events.md +1081 -0
- data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
- data/docs/use_cases/UC-020-event-versioning.md +708 -0
- data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
- data/docs/use_cases/UC-022-event-registry.md +648 -0
- data/docs/use_cases/backlog.md +226 -0
- data/e11y.gemspec +76 -0
- data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
- data/lib/e11y/adapters/audit_encrypted.rb +239 -0
- data/lib/e11y/adapters/base.rb +580 -0
- data/lib/e11y/adapters/file.rb +224 -0
- data/lib/e11y/adapters/in_memory.rb +216 -0
- data/lib/e11y/adapters/loki.rb +333 -0
- data/lib/e11y/adapters/otel_logs.rb +203 -0
- data/lib/e11y/adapters/registry.rb +141 -0
- data/lib/e11y/adapters/sentry.rb +230 -0
- data/lib/e11y/adapters/stdout.rb +108 -0
- data/lib/e11y/adapters/yabeda.rb +370 -0
- data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
- data/lib/e11y/buffers/base_buffer.rb +40 -0
- data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
- data/lib/e11y/buffers/ring_buffer.rb +267 -0
- data/lib/e11y/buffers.rb +14 -0
- data/lib/e11y/console.rb +122 -0
- data/lib/e11y/current.rb +48 -0
- data/lib/e11y/event/base.rb +894 -0
- data/lib/e11y/event/value_sampling_config.rb +84 -0
- data/lib/e11y/events/base_audit_event.rb +43 -0
- data/lib/e11y/events/base_payment_event.rb +33 -0
- data/lib/e11y/events/rails/cache/delete.rb +21 -0
- data/lib/e11y/events/rails/cache/read.rb +23 -0
- data/lib/e11y/events/rails/cache/write.rb +22 -0
- data/lib/e11y/events/rails/database/query.rb +45 -0
- data/lib/e11y/events/rails/http/redirect.rb +21 -0
- data/lib/e11y/events/rails/http/request.rb +26 -0
- data/lib/e11y/events/rails/http/send_file.rb +21 -0
- data/lib/e11y/events/rails/http/start_processing.rb +26 -0
- data/lib/e11y/events/rails/job/completed.rb +22 -0
- data/lib/e11y/events/rails/job/enqueued.rb +22 -0
- data/lib/e11y/events/rails/job/failed.rb +22 -0
- data/lib/e11y/events/rails/job/scheduled.rb +23 -0
- data/lib/e11y/events/rails/job/started.rb +22 -0
- data/lib/e11y/events/rails/log.rb +56 -0
- data/lib/e11y/events/rails/view/render.rb +23 -0
- data/lib/e11y/events.rb +18 -0
- data/lib/e11y/instruments/active_job.rb +201 -0
- data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
- data/lib/e11y/instruments/sidekiq.rb +175 -0
- data/lib/e11y/logger/bridge.rb +205 -0
- data/lib/e11y/metrics/cardinality_protection.rb +172 -0
- data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
- data/lib/e11y/metrics/registry.rb +234 -0
- data/lib/e11y/metrics/relabeling.rb +226 -0
- data/lib/e11y/metrics.rb +102 -0
- data/lib/e11y/middleware/audit_signing.rb +174 -0
- data/lib/e11y/middleware/base.rb +140 -0
- data/lib/e11y/middleware/event_slo.rb +167 -0
- data/lib/e11y/middleware/pii_filter.rb +266 -0
- data/lib/e11y/middleware/pii_filtering.rb +280 -0
- data/lib/e11y/middleware/rate_limiting.rb +214 -0
- data/lib/e11y/middleware/request.rb +163 -0
- data/lib/e11y/middleware/routing.rb +157 -0
- data/lib/e11y/middleware/sampling.rb +254 -0
- data/lib/e11y/middleware/slo.rb +168 -0
- data/lib/e11y/middleware/trace_context.rb +131 -0
- data/lib/e11y/middleware/validation.rb +118 -0
- data/lib/e11y/middleware/versioning.rb +132 -0
- data/lib/e11y/middleware.rb +12 -0
- data/lib/e11y/pii/patterns.rb +90 -0
- data/lib/e11y/pii.rb +13 -0
- data/lib/e11y/pipeline/builder.rb +155 -0
- data/lib/e11y/pipeline/zone_validator.rb +110 -0
- data/lib/e11y/pipeline.rb +12 -0
- data/lib/e11y/presets/audit_event.rb +65 -0
- data/lib/e11y/presets/debug_event.rb +34 -0
- data/lib/e11y/presets/high_value_event.rb +51 -0
- data/lib/e11y/presets.rb +19 -0
- data/lib/e11y/railtie.rb +138 -0
- data/lib/e11y/reliability/circuit_breaker.rb +216 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
- data/lib/e11y/reliability/dlq/filter.rb +117 -0
- data/lib/e11y/reliability/retry_handler.rb +207 -0
- data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
- data/lib/e11y/sampling/error_spike_detector.rb +225 -0
- data/lib/e11y/sampling/load_monitor.rb +161 -0
- data/lib/e11y/sampling/stratified_tracker.rb +92 -0
- data/lib/e11y/sampling/value_extractor.rb +82 -0
- data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
- data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
- data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
- data/lib/e11y/slo/event_driven.rb +150 -0
- data/lib/e11y/slo/tracker.rb +119 -0
- data/lib/e11y/version.rb +9 -0
- data/lib/e11y.rb +283 -0
- 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
|