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,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
|