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,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "fileutils"
5
+ require "json"
6
+ require "zlib"
7
+
8
+ module E11y
9
+ module Adapters
10
+ # File adapter for writing events to local files with rotation and compression.
11
+ #
12
+ # Features:
13
+ # - JSONL format (one JSON object per line)
14
+ # - Automatic rotation (by size, time, or daily)
15
+ # - Optional gzip compression on rotation
16
+ # - Thread-safe writes
17
+ # - Batch write support
18
+ #
19
+ # @example Basic usage
20
+ # adapter = E11y::Adapters::File.new(
21
+ # path: "log/e11y.log",
22
+ # rotation: :daily,
23
+ # max_size: 100 * 1024 * 1024, # 100MB
24
+ # compress: true
25
+ # )
26
+ #
27
+ # adapter.write(event_name: "user.login", severity: :info)
28
+ #
29
+ # @example With Registry
30
+ # E11y::Adapters::Registry.register(
31
+ # :file_logger,
32
+ # E11y::Adapters::File.new(path: "log/events.log")
33
+ # )
34
+ class File < Base
35
+ # Default maximum file size before rotation (100MB)
36
+ DEFAULT_MAX_SIZE = 100 * 1024 * 1024
37
+
38
+ # Default rotation strategy
39
+ DEFAULT_ROTATION = :daily
40
+
41
+ attr_reader :path, :rotation, :max_size, :compress_on_rotate
42
+
43
+ # Initialize File adapter
44
+ #
45
+ # @param config [Hash] Configuration options
46
+ # @option config [String] :path (required) Path to log file
47
+ # @option config [Symbol] :rotation (:daily) Rotation strategy (:daily, :size, :none)
48
+ # @option config [Integer] :max_size (100MB) Max file size before rotation (for :size strategy)
49
+ # @option config [Boolean] :compress (true) Compress rotated files with gzip
50
+ def initialize(config = {})
51
+ @path = config[:path]
52
+ @rotation = config.fetch(:rotation, DEFAULT_ROTATION)
53
+ @max_size = config.fetch(:max_size, DEFAULT_MAX_SIZE)
54
+ @compress_on_rotate = config.fetch(:compress, true)
55
+ @file = nil
56
+ @mutex = Mutex.new
57
+ @current_date = nil
58
+
59
+ super
60
+
61
+ ensure_directory!
62
+ open_file!
63
+ end
64
+
65
+ # Write a single event to file
66
+ #
67
+ # @param event_data [Hash] Event payload
68
+ # @return [Boolean] Success status
69
+ def write(event_data)
70
+ @mutex.synchronize do
71
+ rotate_if_needed!
72
+
73
+ line = format_event(event_data)
74
+ @file.puts(line)
75
+ @file.flush
76
+ end
77
+
78
+ true
79
+ rescue StandardError => e
80
+ warn "E11y File adapter error: #{e.message}"
81
+ false
82
+ end
83
+
84
+ # Write a batch of events to file
85
+ #
86
+ # @param events [Array<Hash>] Array of event payloads
87
+ # @return [Boolean] Success status
88
+ def write_batch(events)
89
+ @mutex.synchronize do
90
+ rotate_if_needed!
91
+
92
+ events.each do |event_data|
93
+ line = format_event(event_data)
94
+ @file.puts(line)
95
+ end
96
+
97
+ @file.flush
98
+ end
99
+
100
+ true
101
+ rescue StandardError => e
102
+ warn "E11y File adapter batch error: #{e.message}"
103
+ false
104
+ end
105
+
106
+ # Close the file handle
107
+ def close
108
+ @mutex.synchronize do
109
+ @file&.close
110
+ @file = nil
111
+ end
112
+ end
113
+
114
+ # Check if adapter is healthy
115
+ #
116
+ # @return [Boolean] True if file is writable
117
+ def healthy?
118
+ @mutex.synchronize do
119
+ return false unless @file
120
+
121
+ !@file.closed?
122
+ end
123
+ end
124
+
125
+ # Adapter capabilities
126
+ #
127
+ # @return [Hash] Capability flags
128
+ def capabilities
129
+ super.merge(
130
+ batching: true,
131
+ compression: @compress_on_rotate,
132
+ streaming: true
133
+ )
134
+ end
135
+
136
+ private
137
+
138
+ # Validate configuration
139
+ def validate_config!
140
+ raise ArgumentError, "File adapter requires :path" unless @path
141
+ raise ArgumentError, "Invalid rotation: #{@rotation}" unless %i[daily size none].include?(@rotation)
142
+ raise ArgumentError, "max_size must be positive" if @max_size && @max_size <= 0
143
+ end
144
+
145
+ # Ensure directory exists
146
+ def ensure_directory!
147
+ dir = ::File.dirname(@path)
148
+ FileUtils.mkdir_p(dir) unless ::File.directory?(dir)
149
+ end
150
+
151
+ # Open file for writing
152
+ def open_file!
153
+ @file = ::File.open(@path, "a")
154
+ @file.sync = true
155
+ @current_date = Date.today if @rotation == :daily
156
+ end
157
+
158
+ # Format event as JSONL
159
+ #
160
+ # @param event_data [Hash] Event payload
161
+ # @return [String] JSON string
162
+ def format_event(event_data)
163
+ event_data.to_json
164
+ end
165
+
166
+ # Check if rotation is needed and perform it
167
+ def rotate_if_needed!
168
+ case @rotation
169
+ when :daily
170
+ rotate_daily!
171
+ when :size
172
+ rotate_by_size!
173
+ end
174
+ end
175
+
176
+ # Rotate file if date changed
177
+ def rotate_daily!
178
+ today = Date.today
179
+ return unless @current_date && today != @current_date
180
+
181
+ perform_rotation!
182
+ @current_date = today
183
+ end
184
+
185
+ # Rotate file if size exceeded
186
+ def rotate_by_size!
187
+ return unless @file.size >= @max_size
188
+
189
+ perform_rotation!
190
+ end
191
+
192
+ # Perform actual file rotation
193
+ def perform_rotation!
194
+ @file.close if @file
195
+
196
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
197
+ rotated_path = "#{@path}.#{timestamp}"
198
+
199
+ ::File.rename(@path, rotated_path) if ::File.exist?(@path)
200
+
201
+ compress_file(rotated_path) if @compress_on_rotate
202
+
203
+ open_file!
204
+ end
205
+
206
+ # Compress rotated file with gzip
207
+ #
208
+ # @param file_path [String] Path to file to compress
209
+ def compress_file(file_path)
210
+ return unless ::File.exist?(file_path)
211
+
212
+ Zlib::GzipWriter.open("#{file_path}.gz") do |gz|
213
+ ::File.open(file_path, "rb") do |file|
214
+ gz.write(file.read)
215
+ end
216
+ end
217
+
218
+ ::File.delete(file_path)
219
+ rescue StandardError => e
220
+ warn "E11y File adapter compression error: #{e.message}"
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Adapters
5
+ # InMemory Adapter - Test adapter for specs and debugging
6
+ #
7
+ # Stores events in memory for testing and inspection.
8
+ # Not for production use - events are lost on restart.
9
+ #
10
+ # **⚠️ Memory Safety:**
11
+ # - Default limit: 1000 events (prevents unbounded growth)
12
+ # - Auto-drops oldest events when limit reached (FIFO)
13
+ # - Configure limit based on test needs
14
+ #
15
+ # **Features:**
16
+ # - Thread-safe event storage
17
+ # - Batch tracking
18
+ # - Query methods for tests
19
+ # - Manual clear support
20
+ # - Automatic memory limit enforcement
21
+ #
22
+ # @example Usage in tests
23
+ # let(:test_adapter) { E11y::Adapters::InMemory.new }
24
+ #
25
+ # before { E11y.register_adapter :test, test_adapter }
26
+ # after { test_adapter.clear! }
27
+ #
28
+ # it "tracks events" do
29
+ # Events::OrderPaid.track(order_id: '123')
30
+ # expect(test_adapter.events.size).to eq(1)
31
+ # expect(test_adapter.events.first[:event_name]).to eq('order.paid')
32
+ # end
33
+ #
34
+ # @example Custom limit
35
+ # # For tests with many events
36
+ # test_adapter = E11y::Adapters::InMemory.new(max_events: 10_000)
37
+ #
38
+ # # Unlimited (use with caution!)
39
+ # test_adapter = E11y::Adapters::InMemory.new(max_events: nil)
40
+ #
41
+ # @see ADR-004 §9.1 (In-Memory Test Adapter)
42
+ class InMemory < Base
43
+ # Default maximum number of events to store
44
+ DEFAULT_MAX_EVENTS = 1000
45
+
46
+ # All events written to adapter
47
+ #
48
+ # @return [Array<Hash>] Array of event payloads
49
+ attr_reader :events
50
+
51
+ # All batches written to adapter
52
+ #
53
+ # @return [Array<Array<Hash>>] Array of event batches
54
+ attr_reader :batches
55
+
56
+ # Maximum number of events to store
57
+ #
58
+ # @return [Integer, nil] Max events or nil for unlimited
59
+ attr_reader :max_events
60
+
61
+ # Number of events dropped due to limit
62
+ #
63
+ # @return [Integer] Dropped event count
64
+ attr_reader :dropped_count
65
+
66
+ # Initialize adapter
67
+ #
68
+ # @param config [Hash] Configuration options
69
+ # @option config [Integer, nil] :max_events (1000) Maximum events to store (nil = unlimited)
70
+ def initialize(config = {})
71
+ super
72
+ @max_events = config.fetch(:max_events, DEFAULT_MAX_EVENTS)
73
+ @events = []
74
+ @batches = []
75
+ @dropped_count = 0
76
+ @mutex = Mutex.new
77
+ end
78
+
79
+ # Write event to memory
80
+ #
81
+ # @param event_data [Hash] Event payload
82
+ # @return [Boolean] true on success
83
+ def write(event_data)
84
+ @mutex.synchronize do
85
+ @events << event_data
86
+ enforce_limit!
87
+ end
88
+ true
89
+ end
90
+
91
+ # Write batch of events to memory
92
+ #
93
+ # @param events [Array<Hash>] Array of event payloads
94
+ # @return [Boolean] true on success
95
+ def write_batch(events)
96
+ @mutex.synchronize do
97
+ @events.concat(events)
98
+ @batches << events
99
+ enforce_limit!
100
+ end
101
+ true
102
+ end
103
+
104
+ # Clear all stored events and batches
105
+ #
106
+ # @return [void]
107
+ def clear!
108
+ @mutex.synchronize do
109
+ @events.clear
110
+ @batches.clear
111
+ @dropped_count = 0
112
+ end
113
+ end
114
+
115
+ # Find events matching pattern
116
+ #
117
+ # @param pattern [String, Regexp] Pattern to match against event_name
118
+ # @return [Array<Hash>] Matching events
119
+ #
120
+ # @example
121
+ # adapter.find_events(/order/) # All order.* events
122
+ # adapter.find_events("order.paid") # Exact match
123
+ def find_events(pattern)
124
+ pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
125
+ @events.select { |event| event[:event_name].to_s.match?(pattern) }
126
+ end
127
+
128
+ # Count events by name
129
+ #
130
+ # @param event_name [String, nil] Event name to count, or nil for total
131
+ # @return [Integer] Event count
132
+ #
133
+ # @example
134
+ # adapter.event_count # Total events
135
+ # adapter.event_count("order.paid") # Specific event count
136
+ def event_count(event_name: nil)
137
+ if event_name
138
+ @events.count { |event| event[:event_name] == event_name }
139
+ else
140
+ @events.size
141
+ end
142
+ end
143
+
144
+ # Get last N events
145
+ #
146
+ # @param count [Integer] Number of events to return
147
+ # @return [Array<Hash>] Last N events
148
+ #
149
+ # @example
150
+ # adapter.last_events(5) # Last 5 events
151
+ def last_events(count = 10)
152
+ @events.last(count)
153
+ end
154
+
155
+ # Get first N events
156
+ #
157
+ # @param count [Integer] Number of events to return
158
+ # @return [Array<Hash>] First N events
159
+ #
160
+ # @example
161
+ # adapter.first_events(5) # First 5 events
162
+ def first_events(count = 10)
163
+ @events.first(count)
164
+ end
165
+
166
+ # Find events by severity
167
+ #
168
+ # @param severity [Symbol] Severity level to filter by
169
+ # @return [Array<Hash>] Events with matching severity
170
+ #
171
+ # @example
172
+ # adapter.events_by_severity(:error) # All error events
173
+ def events_by_severity(severity)
174
+ @events.select { |event| event[:severity] == severity }
175
+ end
176
+
177
+ # Check if any events match pattern
178
+ #
179
+ # @param pattern [String, Regexp] Pattern to match
180
+ # @return [Boolean] true if any events match
181
+ #
182
+ # @example
183
+ # adapter.any_event?(/order/) # Any order.* events?
184
+ def any_event?(pattern)
185
+ find_events(pattern).any?
186
+ end
187
+
188
+ # Adapter capabilities
189
+ #
190
+ # @return [Hash] Capability flags
191
+ def capabilities
192
+ {
193
+ batching: true,
194
+ compression: false,
195
+ async: false,
196
+ streaming: false
197
+ }
198
+ end
199
+
200
+ private
201
+
202
+ # Enforce max_events limit by dropping oldest events (FIFO)
203
+ #
204
+ # @return [void]
205
+ def enforce_limit!
206
+ return if max_events.nil? # Unlimited
207
+
208
+ return unless @events.size > max_events
209
+
210
+ excess = @events.size - max_events
211
+ @events.shift(excess)
212
+ @dropped_count += excess
213
+ end
214
+ end
215
+ end
216
+ end