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,2385 @@
1
+ # ADR-004: Adapter Architecture
2
+
3
+ **Status:** ✅ Implemented (Phase L2.5 Complete)
4
+ **Date:** January 12, 2026 | **Updated:** January 19, 2026
5
+ **Covers:** UC-005 (Sentry Integration), All Adapters (Loki, File, Stdout, InMemory, AuditEncrypted)
6
+ **Depends On:** ADR-001 (Core Architecture), ADR-002 (Metrics)
7
+
8
+ **Implementation Status:**
9
+ - ✅ §3.1: Base Adapter Contract - Implemented
10
+ - ✅ §4.1: Stdout Adapter - Implemented (29 tests)
11
+ - ✅ §4.2: File Adapter - Implemented (35 tests)
12
+ - ✅ §4.3: Loki Adapter - Implemented (34 tests)
13
+ - ✅ §4.4: Sentry Adapter - Implemented (39 tests)
14
+ - ❌ §4.5: Elasticsearch Adapter - Cancelled (not needed now)
15
+ - ✅ §5: Adapter Registry - Implemented (26 tests)
16
+ - ✅ §9.1: InMemory Test Adapter - Implemented (51 tests)
17
+ - ✅ AuditEncrypted Adapter - Updated to new contract (13 tests)
18
+
19
+ **Test Coverage:** 249/249 adapter tests passing
20
+
21
+ ---
22
+
23
+ ## 📋 Table of Contents
24
+
25
+ 1. [Context & Problem](#1-context--problem)
26
+ 2. [Architecture Overview](#2-architecture-overview)
27
+ 3. [Adapter Interface](#3-adapter-interface)
28
+ 4. [Built-in Adapters](#4-built-in-adapters)
29
+ - 4.1. [Stdout Adapter](#41-stdout-adapter)
30
+ - 4.2. [File Adapter](#42-file-adapter)
31
+ - 4.3. [Loki Adapter](#43-loki-adapter)
32
+ - 4.4. [Sentry Adapter](#44-sentry-adapter)
33
+ - 4.5. [Elasticsearch Adapter](#45-elasticsearch-adapter)
34
+ 5. [Adapter Registry](#5-adapter-registry)
35
+ 6. [Connection Management](#6-connection-management)
36
+ 7. [Error Handling & Retry](#7-error-handling--retry)
37
+ 8. [Performance & Batching](#8-performance--batching)
38
+ 9. [Testing Strategy](#9-testing-strategy)
39
+ 10. [Custom Adapters](#10-custom-adapters)
40
+ 11. [Trade-offs](#11-trade-offs)
41
+
42
+ ---
43
+
44
+ ## 1. Context & Problem
45
+
46
+ ### 1.1. Problem Statement
47
+
48
+ **Current Pain Points:**
49
+
50
+ 1. **Tight Coupling:**
51
+ ```ruby
52
+ # ❌ Hard-coded destinations
53
+ Events::OrderPaid.track(...) do
54
+ send_to_loki(...)
55
+ send_to_sentry(...)
56
+ send_to_file(...)
57
+ end
58
+ ```
59
+
60
+ 2. **No Unified Interface:**
61
+ - Each destination has different API
62
+ - No standard error handling
63
+ - Inconsistent retry logic
64
+
65
+ 3. **Configuration Duplication:**
66
+ ```ruby
67
+ # ❌ Configure same adapter multiple times
68
+ Events::OrderPaid.track(..., adapters: [Loki.new(...)])
69
+ Events::PaymentFailed.track(..., adapters: [Loki.new(...)])
70
+ ```
71
+
72
+ 4. **Testing Complexity:**
73
+ - Can't easily mock adapters
74
+ - Integration tests require real services
75
+
76
+ ### 1.2. Goals
77
+
78
+ **Primary Goals:**
79
+ - ✅ Unified adapter interface (contract-based)
80
+ - ✅ Global adapter registry (configure once, reference by name)
81
+ - ✅ Per-event adapter overrides
82
+ - ✅ Built-in error handling & retry
83
+ - ✅ Connection pooling & reuse
84
+ - ✅ Easy testing (in-memory adapter)
85
+
86
+ **Non-Goals:**
87
+ - ❌ Support every possible destination (only critical ones)
88
+ - ❌ Real-time guarantees (async buffering is OK)
89
+ - ❌ Perfect delivery (at-least-once, not exactly-once)
90
+
91
+ ### 1.3. Success Metrics
92
+
93
+ | Metric | Target | Critical? |
94
+ |--------|--------|-----------|
95
+ | **Adapter overhead** | <0.5ms per event | ✅ Yes |
96
+ | **Connection reuse** | 100% | ✅ Yes |
97
+ | **Retry success rate** | >95% | ✅ Yes |
98
+ | **Test coverage** | 100% for base | ✅ Yes |
99
+
100
+ ---
101
+
102
+ ## 2. Architecture Overview
103
+
104
+ ### 2.1. System Context
105
+
106
+ ```mermaid
107
+ C4Context
108
+ title Adapter System Context
109
+
110
+ Person(dev, "Developer", "Tracks events")
111
+
112
+ System(e11y, "E11y Gem", "Event tracking system")
113
+
114
+ System_Ext(loki, "Grafana Loki", "Log aggregation")
115
+ System_Ext(sentry, "Sentry", "Error tracking")
116
+ System_Ext(es, "Elasticsearch", "Search & analytics")
117
+ System_Ext(file, "File System", "Local logs")
118
+ System_Ext(stdout, "Stdout", "Console output")
119
+
120
+ Rel(dev, e11y, "Tracks events", "Events::OrderPaid.track(...)")
121
+
122
+ Rel(e11y, loki, "Ships logs", "HTTP POST /loki/api/v1/push")
123
+ Rel(e11y, sentry, "Reports errors", "Sentry SDK")
124
+ Rel(e11y, es, "Indexes events", "HTTP POST /_bulk")
125
+ Rel(e11y, file, "Writes logs", "File I/O")
126
+ Rel(e11y, stdout, "Prints events", "STDOUT")
127
+ ```
128
+
129
+ ### 2.2. Component Architecture
130
+
131
+ ```mermaid
132
+ graph TB
133
+ subgraph "E11y Core"
134
+ Event[Event.track] --> Pipeline[Middleware Chain]
135
+ Pipeline --> Adapters[Adapter Middleware]
136
+ end
137
+
138
+ subgraph "Adapter Layer"
139
+ Adapters --> Registry[Adapter Registry]
140
+ Registry --> Router[Adapter Router]
141
+
142
+ Router --> Batch[Batching Layer]
143
+ Batch --> Pool[Connection Pool]
144
+ end
145
+
146
+ subgraph "Built-in Adapters"
147
+ Pool --> Stdout[Stdout Adapter]
148
+ Pool --> File[File Adapter]
149
+ Pool --> Loki[Loki Adapter]
150
+ Pool --> Sentry[Sentry Adapter]
151
+ Pool --> ES[Elasticsearch Adapter]
152
+ end
153
+
154
+ subgraph "Support Components"
155
+ Pool --> Retry[Retry Handler]
156
+ Pool --> Circuit[Circuit Breaker]
157
+ Pool --> Compress[Compression]
158
+ end
159
+
160
+ Retry --> DLQ[Dead Letter Queue]
161
+
162
+ style Registry fill:#d1ecf1
163
+ style Router fill:#fff3cd
164
+ style Batch fill:#d4edda
165
+ style Pool fill:#f8d7da
166
+ ```
167
+
168
+ ### 2.3. Data Flow
169
+
170
+ ```mermaid
171
+ sequenceDiagram
172
+ participant App as Application
173
+ participant Event as Events::OrderPaid
174
+ participant Router as Adapter Router
175
+ participant Batch as Batching Layer
176
+ participant Loki as Loki Adapter
177
+ participant HTTP as HTTP Client
178
+ participant Backend as Loki Backend
179
+
180
+ App->>Event: .track(order_id: '123', amount: 99.99)
181
+ Event->>Router: route(event_data, adapters: [:loki, :sentry])
182
+
183
+ Router->>Router: Resolve adapter names → instances
184
+
185
+ loop For each adapter
186
+ Router->>Batch: enqueue(event_data, adapter: loki)
187
+ Batch->>Batch: Buffer until threshold
188
+
189
+ Note over Batch: Batch size = 100 or 5s timeout
190
+
191
+ Batch->>Loki: write_batch([event1, event2, ...])
192
+ Loki->>Loki: Format for Loki API
193
+ Loki->>Loki: Compress (gzip)
194
+
195
+ Loki->>HTTP: POST /loki/api/v1/push
196
+ HTTP->>Backend: Send batch
197
+
198
+ alt Success
199
+ Backend-->>HTTP: 204 No Content
200
+ HTTP-->>Loki: OK
201
+ Loki-->>Batch: Success
202
+ else Failure (retriable)
203
+ Backend-->>HTTP: 429 Rate Limited
204
+ HTTP-->>Loki: Retry
205
+ Loki->>Loki: Exponential backoff
206
+ Loki->>HTTP: POST /loki/api/v1/push (retry)
207
+ else Failure (permanent)
208
+ Backend-->>HTTP: 400 Bad Request
209
+ HTTP-->>Loki: Fail
210
+ Loki->>DLQ: Send to DLQ
211
+ end
212
+ end
213
+ ```
214
+
215
+ ---
216
+
217
+ ## 3. Adapter Interface
218
+
219
+ ### 3.1. Base Adapter Contract
220
+
221
+ ```ruby
222
+ module E11y
223
+ module Adapters
224
+ class Base
225
+ # Initialize adapter with config
226
+ # @param config [Hash] Adapter-specific configuration
227
+ def initialize(config = {})
228
+ @config = config
229
+ validate_config!
230
+ end
231
+
232
+ # Write a single event (synchronous)
233
+ # @param event_data [Hash] Event payload
234
+ # @return [Boolean] Success
235
+ def write(event_data)
236
+ raise NotImplementedError, "Adapters must implement #write"
237
+ end
238
+
239
+ # Write a batch of events (preferred for performance)
240
+ # @param events [Array<Hash>] Array of event payloads
241
+ # @return [Boolean] Success
242
+ def write_batch(events)
243
+ # Default: call write for each event
244
+ events.all? { |event| write(event) }
245
+ end
246
+
247
+ # Check if adapter is healthy
248
+ # @return [Boolean] Health status
249
+ def healthy?
250
+ true
251
+ end
252
+
253
+ # Close connections, flush buffers
254
+ def close
255
+ # Default: no-op
256
+ end
257
+
258
+ # Adapter capabilities
259
+ # @return [Hash] Capability flags
260
+ def capabilities
261
+ {
262
+ batching: false,
263
+ compression: false,
264
+ async: false,
265
+ streaming: false
266
+ }
267
+ end
268
+
269
+ private
270
+
271
+ # Validate adapter config
272
+ def validate_config!
273
+ # Override in subclasses
274
+ end
275
+
276
+ # Format event for this adapter
277
+ # @param event_data [Hash]
278
+ # @return [Hash, String] Formatted event
279
+ def format_event(event_data)
280
+ event_data
281
+ end
282
+ end
283
+ end
284
+ end
285
+ ```
286
+
287
+ ### 3.2. Adapter Lifecycle
288
+
289
+ ```mermaid
290
+ stateDiagram-v2
291
+ [*] --> Initializing
292
+
293
+ Initializing --> Connecting: initialize(config)
294
+ Connecting --> Ready: Connection established
295
+ Connecting --> Failed: Connection failed
296
+
297
+ Ready --> Writing: write(event)
298
+ Writing --> Ready: Success
299
+ Writing --> Retrying: Retriable error
300
+ Writing --> Failed: Permanent error
301
+
302
+ Retrying --> Writing: Retry attempt
303
+ Retrying --> Failed: Max retries exceeded
304
+
305
+ Ready --> Closing: close()
306
+ Closing --> Closed: Cleanup complete
307
+
308
+ Failed --> Closing: Manual close
309
+ Closed --> [*]
310
+
311
+ note right of Ready
312
+ Healthy state
313
+ Can process events
314
+ end note
315
+
316
+ note right of Failed
317
+ Circuit breaker open
318
+ Events go to DLQ
319
+ end note
320
+ ```
321
+
322
+ ---
323
+
324
+ ## 4. Built-in Adapters
325
+
326
+ ### 4.1. Stdout Adapter
327
+
328
+ **Purpose:** Console output for development and debugging.
329
+
330
+ ```ruby
331
+ module E11y
332
+ module Adapters
333
+ class Stdout < Base
334
+ def initialize(config = {})
335
+ super
336
+ @colorize = config.fetch(:colorize, true)
337
+ @pretty_print = config.fetch(:pretty_print, true)
338
+ end
339
+
340
+ def write(event_data)
341
+ output = format_event(event_data)
342
+
343
+ if @colorize
344
+ puts colorize_output(output)
345
+ else
346
+ puts output
347
+ end
348
+
349
+ true
350
+ rescue => e
351
+ warn "Stdout adapter error: #{e.message}"
352
+ false
353
+ end
354
+
355
+ def capabilities
356
+ {
357
+ batching: false,
358
+ compression: false,
359
+ async: false,
360
+ streaming: true
361
+ }
362
+ end
363
+
364
+ private
365
+
366
+ def format_event(event_data)
367
+ if @pretty_print
368
+ JSON.pretty_generate(event_data)
369
+ else
370
+ event_data.to_json
371
+ end
372
+ end
373
+
374
+ def colorize_output(output)
375
+ severity = event_data[:severity]
376
+
377
+ case severity
378
+ when :debug then "\e[37m#{output}\e[0m" # Gray
379
+ when :info then "\e[36m#{output}\e[0m" # Cyan
380
+ when :success then "\e[32m#{output}\e[0m" # Green
381
+ when :warn then "\e[33m#{output}\e[0m" # Yellow
382
+ when :error then "\e[31m#{output}\e[0m" # Red
383
+ when :fatal then "\e[35m#{output}\e[0m" # Magenta
384
+ else output
385
+ end
386
+ end
387
+ end
388
+ end
389
+ end
390
+ ```
391
+
392
+ **Configuration:**
393
+
394
+ ```ruby
395
+ E11y.configure do |config|
396
+ config.adapters do
397
+ register :stdout, E11y::Adapters::Stdout.new(
398
+ colorize: true,
399
+ pretty_print: true
400
+ )
401
+ end
402
+ end
403
+ ```
404
+
405
+ ---
406
+
407
+ ### 4.2. File Adapter
408
+
409
+ **Purpose:** Write events to local files with rotation.
410
+
411
+ ```ruby
412
+ module E11y
413
+ module Adapters
414
+ class File < Base
415
+ def initialize(config = {})
416
+ super
417
+ @path = config.fetch(:path)
418
+ @rotation = config.fetch(:rotation, :daily)
419
+ @max_size = config.fetch(:max_size, 100.megabytes)
420
+ @compress_on_rotate = config.fetch(:compress, true)
421
+ @file = nil
422
+ @mutex = Mutex.new
423
+
424
+ ensure_directory!
425
+ open_file!
426
+ end
427
+
428
+ def write(event_data)
429
+ @mutex.synchronize do
430
+ rotate_if_needed!
431
+
432
+ line = format_event(event_data)
433
+ @file.puts(line)
434
+ @file.flush
435
+ end
436
+
437
+ true
438
+ rescue => e
439
+ warn "File adapter error: #{e.message}"
440
+ false
441
+ end
442
+
443
+ def write_batch(events)
444
+ @mutex.synchronize do
445
+ rotate_if_needed!
446
+
447
+ events.each do |event_data|
448
+ line = format_event(event_data)
449
+ @file.puts(line)
450
+ end
451
+
452
+ @file.flush
453
+ end
454
+
455
+ true
456
+ rescue => e
457
+ warn "File adapter batch error: #{e.message}"
458
+ false
459
+ end
460
+
461
+ def close
462
+ @mutex.synchronize do
463
+ @file&.close
464
+ @file = nil
465
+ end
466
+ end
467
+
468
+ def capabilities
469
+ {
470
+ batching: true,
471
+ compression: false, # Done on rotation
472
+ async: false,
473
+ streaming: true
474
+ }
475
+ end
476
+
477
+ private
478
+
479
+ def format_event(event_data)
480
+ event_data.to_json
481
+ end
482
+
483
+ def ensure_directory!
484
+ dir = ::File.dirname(@path)
485
+ FileUtils.mkdir_p(dir) unless ::File.directory?(dir)
486
+ end
487
+
488
+ def open_file!
489
+ @file = ::File.open(current_path, 'a')
490
+ @file.sync = true # Auto-flush
491
+ end
492
+
493
+ def current_path
494
+ case @rotation
495
+ when :daily
496
+ "#{@path}.#{Date.today.strftime('%Y%m%d')}"
497
+ when :hourly
498
+ "#{@path}.#{Time.now.strftime('%Y%m%d%H')}"
499
+ else
500
+ @path
501
+ end
502
+ end
503
+
504
+ def rotate_if_needed!
505
+ need_rotation = case @rotation
506
+ when :daily
507
+ @file.path != current_path
508
+ when :hourly
509
+ @file.path != current_path
510
+ when :size
511
+ @file.size >= @max_size
512
+ else
513
+ false
514
+ end
515
+
516
+ if need_rotation
517
+ rotate!
518
+ end
519
+ end
520
+
521
+ def rotate!
522
+ old_path = @file.path
523
+
524
+ @file.close
525
+
526
+ # Compress old file
527
+ if @compress_on_rotate
528
+ compress_file!(old_path)
529
+ end
530
+
531
+ # Open new file
532
+ open_file!
533
+ end
534
+
535
+ def compress_file!(path)
536
+ require 'zlib'
537
+
538
+ Zlib::GzipWriter.open("#{path}.gz") do |gz|
539
+ ::File.open(path, 'r') do |file|
540
+ gz.write(file.read)
541
+ end
542
+ end
543
+
544
+ ::File.delete(path)
545
+ end
546
+ end
547
+ end
548
+ end
549
+ ```
550
+
551
+ **Configuration:**
552
+
553
+ ```ruby
554
+ E11y.configure do |config|
555
+ config.adapters do
556
+ register :file, E11y::Adapters::File.new(
557
+ path: Rails.root.join('log', 'e11y.log'),
558
+ rotation: :daily,
559
+ max_size: 100.megabytes,
560
+ compress: true
561
+ )
562
+ end
563
+ end
564
+ ```
565
+
566
+ ---
567
+
568
+ ### 4.3. Loki Adapter
569
+
570
+ **Purpose:** Ship logs to Grafana Loki for aggregation and querying.
571
+
572
+ ```ruby
573
+ module E11y
574
+ module Adapters
575
+ class Loki < Base
576
+ def initialize(config = {})
577
+ super
578
+ @url = config.fetch(:url)
579
+ @tenant_id = config[:tenant_id]
580
+ @labels = config.fetch(:labels, {})
581
+ @batch_size = config.fetch(:batch_size, 100)
582
+ @batch_timeout = config.fetch(:batch_timeout, 5.seconds)
583
+ @compress = config.fetch(:compress, true)
584
+
585
+ @connection = build_connection
586
+ @buffer = []
587
+ @buffer_mutex = Mutex.new
588
+ end
589
+
590
+ def write(event_data)
591
+ @buffer_mutex.synchronize do
592
+ @buffer << event_data
593
+
594
+ if @buffer.size >= @batch_size
595
+ flush_buffer!
596
+ end
597
+ end
598
+
599
+ true
600
+ end
601
+
602
+ def write_batch(events)
603
+ @buffer_mutex.synchronize do
604
+ @buffer.concat(events)
605
+
606
+ if @buffer.size >= @batch_size
607
+ flush_buffer!
608
+ end
609
+ end
610
+
611
+ true
612
+ end
613
+
614
+ def close
615
+ @buffer_mutex.synchronize do
616
+ flush_buffer! if @buffer.any?
617
+ end
618
+ end
619
+
620
+ def capabilities
621
+ {
622
+ batching: true,
623
+ compression: true,
624
+ async: true,
625
+ streaming: false
626
+ }
627
+ end
628
+
629
+ private
630
+
631
+ def build_connection
632
+ require 'net/http'
633
+ require 'uri'
634
+
635
+ uri = URI.parse(@url)
636
+
637
+ http = Net::HTTP.new(uri.host, uri.port)
638
+ http.use_ssl = uri.scheme == 'https'
639
+ http.open_timeout = 5
640
+ http.read_timeout = 10
641
+
642
+ http
643
+ end
644
+
645
+ def flush_buffer!
646
+ return if @buffer.empty?
647
+
648
+ events = @buffer.dup
649
+ @buffer.clear
650
+
651
+ # Release mutex before I/O
652
+ @buffer_mutex.unlock
653
+
654
+ begin
655
+ send_to_loki(events)
656
+ ensure
657
+ @buffer_mutex.lock
658
+ end
659
+ end
660
+
661
+ def send_to_loki(events)
662
+ payload = build_loki_payload(events)
663
+ body = payload.to_json
664
+
665
+ # Compress if enabled
666
+ if @compress
667
+ require 'zlib'
668
+ body = Zlib.gzip(body)
669
+ end
670
+
671
+ # Build request
672
+ request = Net::HTTP::Post.new('/loki/api/v1/push')
673
+ request['Content-Type'] = 'application/json'
674
+ request['Content-Encoding'] = 'gzip' if @compress
675
+ request['X-Scope-OrgID'] = @tenant_id if @tenant_id
676
+ request.body = body
677
+
678
+ # Send
679
+ response = @connection.request(request)
680
+
681
+ unless response.is_a?(Net::HTTPSuccess)
682
+ raise "Loki error: #{response.code} #{response.message}"
683
+ end
684
+
685
+ true
686
+ rescue => e
687
+ warn "Loki adapter error: #{e.message}"
688
+ false
689
+ end
690
+
691
+ def build_loki_payload(events)
692
+ # Group events by labels
693
+ streams = events.group_by { |event| extract_labels(event) }
694
+
695
+ {
696
+ streams: streams.map do |labels, stream_events|
697
+ {
698
+ stream: labels,
699
+ values: stream_events.map do |event|
700
+ [
701
+ (event[:timestamp].to_f * 1_000_000_000).to_i.to_s, # Nanoseconds
702
+ event.to_json
703
+ ]
704
+ end
705
+ }
706
+ end
707
+ }
708
+ end
709
+
710
+ def extract_labels(event_data)
711
+ base_labels = @labels.dup
712
+
713
+ # Add standard labels
714
+ base_labels[:severity] = event_data[:severity].to_s
715
+ base_labels[:event_name] = event_data[:event_name]
716
+ base_labels[:service] = event_data[:service] || 'unknown'
717
+ base_labels[:environment] = event_data[:environment] || 'unknown'
718
+
719
+ base_labels
720
+ end
721
+ end
722
+ end
723
+ end
724
+ ```
725
+
726
+ **Configuration:**
727
+
728
+ ```ruby
729
+ E11y.configure do |config|
730
+ config.adapters do
731
+ register :loki, E11y::Adapters::Loki.new(
732
+ url: ENV['LOKI_URL'] || 'http://localhost:3100',
733
+ tenant_id: ENV['LOKI_TENANT_ID'],
734
+ labels: {
735
+ app: 'my_app',
736
+ env: Rails.env
737
+ },
738
+ batch_size: 100,
739
+ batch_timeout: 5.seconds,
740
+ compress: true
741
+ )
742
+ end
743
+ end
744
+ ```
745
+
746
+ ---
747
+
748
+ ### 4.4. Sentry Adapter
749
+
750
+ **Purpose:** Report errors and exceptions to Sentry.
751
+
752
+ ```ruby
753
+ module E11y
754
+ module Adapters
755
+ class Sentry < Base
756
+ def initialize(config = {})
757
+ super
758
+ @dsn = config.fetch(:dsn)
759
+ @environment = config.fetch(:environment, Rails.env)
760
+ @send_breadcrumbs = config.fetch(:breadcrumbs, true)
761
+ @severity_threshold = config.fetch(:severity_threshold, :warn)
762
+
763
+ initialize_sentry!
764
+ end
765
+
766
+ def write(event_data)
767
+ severity = event_data[:severity]
768
+
769
+ # Only send warn+ to Sentry (unless configured otherwise)
770
+ return true unless should_send_to_sentry?(severity)
771
+
772
+ if severity == :error || severity == :fatal
773
+ send_error_to_sentry(event_data)
774
+ else
775
+ send_breadcrumb_to_sentry(event_data)
776
+ end
777
+
778
+ true
779
+ rescue => e
780
+ warn "Sentry adapter error: #{e.message}"
781
+ false
782
+ end
783
+
784
+ def capabilities
785
+ {
786
+ batching: false, # Sentry SDK handles batching
787
+ compression: false, # Sentry SDK handles compression
788
+ async: true, # Sentry SDK is async
789
+ streaming: false
790
+ }
791
+ end
792
+
793
+ private
794
+
795
+ def initialize_sentry!
796
+ require 'sentry-ruby'
797
+
798
+ ::Sentry.init do |config|
799
+ config.dsn = @dsn
800
+ config.environment = @environment
801
+ config.breadcrumbs_logger = [] # We manage breadcrumbs
802
+ end
803
+ end
804
+
805
+ def should_send_to_sentry?(severity)
806
+ severity_levels = [:debug, :info, :success, :warn, :error, :fatal]
807
+ threshold_index = severity_levels.index(@severity_threshold)
808
+ current_index = severity_levels.index(severity)
809
+
810
+ current_index >= threshold_index
811
+ end
812
+
813
+ def send_error_to_sentry(event_data)
814
+ ::Sentry.with_scope do |scope|
815
+ # Set context
816
+ scope.set_tags(extract_tags(event_data))
817
+ scope.set_extras(event_data[:payload] || {})
818
+ scope.set_user(event_data[:user] || {})
819
+
820
+ # Set trace context
821
+ if event_data[:trace_id]
822
+ scope.set_context('trace', {
823
+ trace_id: event_data[:trace_id],
824
+ span_id: event_data[:span_id]
825
+ })
826
+ end
827
+
828
+ # Capture exception or message
829
+ if event_data[:exception]
830
+ ::Sentry.capture_exception(event_data[:exception])
831
+ else
832
+ ::Sentry.capture_message(
833
+ event_data[:message] || event_data[:event_name],
834
+ level: sentry_level(event_data[:severity])
835
+ )
836
+ end
837
+ end
838
+ end
839
+
840
+ def send_breadcrumb_to_sentry(event_data)
841
+ return unless @send_breadcrumbs
842
+
843
+ ::Sentry.add_breadcrumb(
844
+ ::Sentry::Breadcrumb.new(
845
+ category: event_data[:event_name],
846
+ message: event_data[:message],
847
+ level: sentry_level(event_data[:severity]),
848
+ data: event_data[:payload] || {},
849
+ timestamp: event_data[:timestamp]
850
+ )
851
+ )
852
+ end
853
+
854
+ def extract_tags(event_data)
855
+ {
856
+ event_name: event_data[:event_name],
857
+ severity: event_data[:severity],
858
+ environment: event_data[:environment]
859
+ }
860
+ end
861
+
862
+ def sentry_level(severity)
863
+ case severity
864
+ when :debug then :debug
865
+ when :info, :success then :info
866
+ when :warn then :warning
867
+ when :error then :error
868
+ when :fatal then :fatal
869
+ else :info
870
+ end
871
+ end
872
+ end
873
+ end
874
+ end
875
+ ```
876
+
877
+ **Configuration:**
878
+
879
+ ```ruby
880
+ E11y.configure do |config|
881
+ config.adapters do
882
+ register :sentry, E11y::Adapters::Sentry.new(
883
+ dsn: ENV['SENTRY_DSN'],
884
+ environment: Rails.env,
885
+ breadcrumbs: true,
886
+ severity_threshold: :warn # Only send warn+ to Sentry
887
+ )
888
+ end
889
+ end
890
+ ```
891
+
892
+ ---
893
+
894
+ ### 4.5. Elasticsearch Adapter
895
+
896
+ **Purpose:** Index events for search and analytics.
897
+
898
+ ```ruby
899
+ module E11y
900
+ module Adapters
901
+ class Elasticsearch < Base
902
+ def initialize(config = {})
903
+ super
904
+ @url = config.fetch(:url)
905
+ @index_prefix = config.fetch(:index_prefix, 'e11y-events')
906
+ @index_rotation = config.fetch(:index_rotation, :daily)
907
+ @batch_size = config.fetch(:batch_size, 100)
908
+ @batch_timeout = config.fetch(:batch_timeout, 5.seconds)
909
+
910
+ @client = build_client
911
+ @buffer = []
912
+ @buffer_mutex = Mutex.new
913
+ end
914
+
915
+ def write(event_data)
916
+ @buffer_mutex.synchronize do
917
+ @buffer << event_data
918
+
919
+ if @buffer.size >= @batch_size
920
+ flush_buffer!
921
+ end
922
+ end
923
+
924
+ true
925
+ end
926
+
927
+ def write_batch(events)
928
+ @buffer_mutex.synchronize do
929
+ @buffer.concat(events)
930
+
931
+ if @buffer.size >= @batch_size
932
+ flush_buffer!
933
+ end
934
+ end
935
+
936
+ true
937
+ end
938
+
939
+ def close
940
+ @buffer_mutex.synchronize do
941
+ flush_buffer! if @buffer.any?
942
+ end
943
+ end
944
+
945
+ def capabilities
946
+ {
947
+ batching: true,
948
+ compression: true,
949
+ async: true,
950
+ streaming: false
951
+ }
952
+ end
953
+
954
+ private
955
+
956
+ def build_client
957
+ require 'elasticsearch'
958
+
959
+ ::Elasticsearch::Client.new(
960
+ url: @url,
961
+ transport_options: {
962
+ request: { timeout: 10 }
963
+ }
964
+ )
965
+ end
966
+
967
+ def flush_buffer!
968
+ return if @buffer.empty?
969
+
970
+ events = @buffer.dup
971
+ @buffer.clear
972
+
973
+ @buffer_mutex.unlock
974
+
975
+ begin
976
+ bulk_index(events)
977
+ ensure
978
+ @buffer_mutex.lock
979
+ end
980
+ end
981
+
982
+ def bulk_index(events)
983
+ # Build bulk payload
984
+ bulk_body = events.flat_map do |event_data|
985
+ [
986
+ { index: { _index: index_name(event_data) } },
987
+ format_for_es(event_data)
988
+ ]
989
+ end
990
+
991
+ # Send bulk request
992
+ response = @client.bulk(body: bulk_body)
993
+
994
+ # Check for errors
995
+ if response['errors']
996
+ handle_bulk_errors(response['items'])
997
+ end
998
+
999
+ true
1000
+ rescue => e
1001
+ warn "Elasticsearch adapter error: #{e.message}"
1002
+ false
1003
+ end
1004
+
1005
+ def index_name(event_data)
1006
+ timestamp = event_data[:timestamp]
1007
+
1008
+ suffix = case @index_rotation
1009
+ when :daily
1010
+ timestamp.strftime('%Y.%m.%d')
1011
+ when :monthly
1012
+ timestamp.strftime('%Y.%m')
1013
+ when :yearly
1014
+ timestamp.strftime('%Y')
1015
+ else
1016
+ 'default'
1017
+ end
1018
+
1019
+ "#{@index_prefix}-#{suffix}"
1020
+ end
1021
+
1022
+ def format_for_es(event_data)
1023
+ {
1024
+ '@timestamp': event_data[:timestamp].iso8601,
1025
+ event_name: event_data[:event_name],
1026
+ severity: event_data[:severity],
1027
+ message: event_data[:message],
1028
+ trace_id: event_data[:trace_id],
1029
+ span_id: event_data[:span_id],
1030
+ payload: event_data[:payload],
1031
+ metadata: event_data[:metadata],
1032
+ environment: event_data[:environment],
1033
+ service: event_data[:service]
1034
+ }
1035
+ end
1036
+
1037
+ def handle_bulk_errors(items)
1038
+ items.each do |item|
1039
+ if item['index'] && item['index']['error']
1040
+ warn "ES index error: #{item['index']['error']}"
1041
+ end
1042
+ end
1043
+ end
1044
+ end
1045
+ end
1046
+ end
1047
+ ```
1048
+
1049
+ **Configuration:**
1050
+
1051
+ ```ruby
1052
+ E11y.configure do |config|
1053
+ config.adapters do
1054
+ register :elasticsearch, E11y::Adapters::Elasticsearch.new(
1055
+ url: ENV['ELASTICSEARCH_URL'] || 'http://localhost:9200',
1056
+ index_prefix: 'e11y-events',
1057
+ index_rotation: :daily,
1058
+ batch_size: 100,
1059
+ batch_timeout: 5.seconds
1060
+ )
1061
+ end
1062
+ end
1063
+ ```
1064
+
1065
+ ---
1066
+
1067
+ ## 5. Adapter Registry
1068
+
1069
+ ### 5.1. Registry Architecture
1070
+
1071
+ ```mermaid
1072
+ graph TB
1073
+ subgraph "Configuration Phase (Boot Time)"
1074
+ Config[Configuration] --> Register[Registry.register]
1075
+ Register --> Instance1[Loki Instance]
1076
+ Register --> Instance2[Sentry Instance]
1077
+ Register --> Instance3[File Instance]
1078
+
1079
+ Instance1 --> Pool1[Connection Pool]
1080
+ Instance2 --> Pool2[Connection Pool]
1081
+ Instance3 --> Pool3[Connection Pool]
1082
+ end
1083
+
1084
+ subgraph "Runtime Phase"
1085
+ Event[Event.track] --> Resolve[Registry.resolve]
1086
+ Resolve -->|:loki| Instance1
1087
+ Resolve -->|:sentry| Instance2
1088
+ Resolve -->|:file| Instance3
1089
+ end
1090
+
1091
+ style Register fill:#d1ecf1
1092
+ style Resolve fill:#fff3cd
1093
+ style Pool1 fill:#d4edda
1094
+ style Pool2 fill:#d4edda
1095
+ style Pool3 fill:#d4edda
1096
+ ```
1097
+
1098
+ ### 5.2. Registry Implementation
1099
+
1100
+ ```ruby
1101
+ module E11y
1102
+ module Adapters
1103
+ class Registry
1104
+ class << self
1105
+ def register(name, adapter_instance)
1106
+ validate_adapter!(adapter_instance)
1107
+
1108
+ adapters[name] = adapter_instance
1109
+
1110
+ # Register cleanup hook
1111
+ at_exit { adapter_instance.close }
1112
+ end
1113
+
1114
+ def resolve(name)
1115
+ adapters.fetch(name) do
1116
+ raise AdapterNotFoundError, "Adapter not found: #{name}"
1117
+ end
1118
+ end
1119
+
1120
+ def resolve_all(names)
1121
+ names.map { |name| resolve(name) }
1122
+ end
1123
+
1124
+ def all
1125
+ adapters.values
1126
+ end
1127
+
1128
+ def names
1129
+ adapters.keys
1130
+ end
1131
+
1132
+ def clear!
1133
+ adapters.each_value(&:close)
1134
+ adapters.clear
1135
+ end
1136
+
1137
+ private
1138
+
1139
+ def adapters
1140
+ @adapters ||= {}
1141
+ end
1142
+
1143
+ def validate_adapter!(adapter)
1144
+ unless adapter.respond_to?(:write)
1145
+ raise ArgumentError, "Adapter must respond to #write"
1146
+ end
1147
+
1148
+ unless adapter.respond_to?(:write_batch)
1149
+ raise ArgumentError, "Adapter must respond to #write_batch"
1150
+ end
1151
+ end
1152
+ end
1153
+ end
1154
+
1155
+ class AdapterNotFoundError < StandardError; end
1156
+ end
1157
+ end
1158
+ ```
1159
+
1160
+ ### 5.3. Usage in Events
1161
+
1162
+ ```ruby
1163
+ module Events
1164
+ class OrderPaid < E11y::Event::Base
1165
+ # Override global adapters
1166
+ adapters [:loki, :file, :sentry]
1167
+
1168
+ schema do
1169
+ required(:order_id).filled(:string)
1170
+ required(:amount).filled(:float)
1171
+ end
1172
+ end
1173
+
1174
+ class DebugEvent < E11y::Event::Base
1175
+ # Only log to file in development
1176
+ adapters [:file, :stdout]
1177
+
1178
+ schema do
1179
+ required(:message).filled(:string)
1180
+ end
1181
+ end
1182
+ end
1183
+ ```
1184
+
1185
+ ---
1186
+
1187
+ ## 6. Connection Management
1188
+
1189
+ ### 6.1. Connection Pooling
1190
+
1191
+ ```ruby
1192
+ module E11y
1193
+ module Adapters
1194
+ class ConnectionPool
1195
+ def initialize(adapter_class, config, pool_size: 5)
1196
+ @adapter_class = adapter_class
1197
+ @config = config
1198
+ @pool_size = pool_size
1199
+ @pool = []
1200
+ @mutex = Mutex.new
1201
+
1202
+ initialize_pool!
1203
+ end
1204
+
1205
+ def with_connection
1206
+ connection = acquire
1207
+
1208
+ begin
1209
+ yield connection
1210
+ ensure
1211
+ release(connection)
1212
+ end
1213
+ end
1214
+
1215
+ def close_all
1216
+ @mutex.synchronize do
1217
+ @pool.each(&:close)
1218
+ @pool.clear
1219
+ end
1220
+ end
1221
+
1222
+ private
1223
+
1224
+ def initialize_pool!
1225
+ @pool_size.times do
1226
+ @pool << create_connection
1227
+ end
1228
+ end
1229
+
1230
+ def create_connection
1231
+ @adapter_class.new(@config)
1232
+ end
1233
+
1234
+ def acquire
1235
+ @mutex.synchronize do
1236
+ connection = @pool.pop
1237
+
1238
+ # Create new if pool exhausted
1239
+ connection ||= create_connection
1240
+
1241
+ connection
1242
+ end
1243
+ end
1244
+
1245
+ def release(connection)
1246
+ @mutex.synchronize do
1247
+ if @pool.size < @pool_size
1248
+ @pool.push(connection)
1249
+ else
1250
+ connection.close
1251
+ end
1252
+ end
1253
+ end
1254
+ end
1255
+ end
1256
+ end
1257
+ ```
1258
+
1259
+ ---
1260
+
1261
+ ## 7. Error Handling & Retry
1262
+
1263
+ ### 7.1. Retry Policy
1264
+
1265
+ ```ruby
1266
+ module E11y
1267
+ module Adapters
1268
+ class RetryHandler
1269
+ def initialize(max_retries: 3, base_delay: 1.0, max_delay: 30.0)
1270
+ @max_retries = max_retries
1271
+ @base_delay = base_delay
1272
+ @max_delay = max_delay
1273
+ end
1274
+
1275
+ def with_retry(adapter_name, &block)
1276
+ attempt = 0
1277
+
1278
+ begin
1279
+ attempt += 1
1280
+ block.call
1281
+ rescue => e
1282
+ if retriable?(e) && attempt <= @max_retries
1283
+ delay = calculate_delay(attempt)
1284
+
1285
+ warn "[E11y] Adapter #{adapter_name} failed (attempt #{attempt}/#{@max_retries}): #{e.message}"
1286
+ warn "[E11y] Retrying in #{delay}s..."
1287
+
1288
+ sleep(delay)
1289
+ retry
1290
+ else
1291
+ raise
1292
+ end
1293
+ end
1294
+ end
1295
+
1296
+ private
1297
+
1298
+ def retriable?(error)
1299
+ # Network errors
1300
+ return true if error.is_a?(Timeout::Error)
1301
+ return true if error.is_a?(Errno::ECONNREFUSED)
1302
+ return true if error.is_a?(Errno::ETIMEDOUT)
1303
+
1304
+ # HTTP errors (if using HTTP client)
1305
+ if defined?(Net::HTTPRetriableError)
1306
+ return true if error.is_a?(Net::HTTPRetriableError)
1307
+ end
1308
+
1309
+ false
1310
+ end
1311
+
1312
+ def calculate_delay(attempt)
1313
+ # Exponential backoff with jitter
1314
+ delay = [@base_delay * (2 ** (attempt - 1)), @max_delay].min
1315
+ jitter = rand * delay * 0.1
1316
+
1317
+ delay + jitter
1318
+ end
1319
+ end
1320
+ end
1321
+ end
1322
+ ```
1323
+
1324
+ ### 7.2. Circuit Breaker
1325
+
1326
+ ```ruby
1327
+ module E11y
1328
+ module Adapters
1329
+ class CircuitBreaker
1330
+ CLOSED = :closed
1331
+ OPEN = :open
1332
+ HALF_OPEN = :half_open
1333
+
1334
+ def initialize(failure_threshold: 5, timeout: 60, success_threshold: 2)
1335
+ @failure_threshold = failure_threshold
1336
+ @timeout = timeout
1337
+ @success_threshold = success_threshold
1338
+
1339
+ @state = CLOSED
1340
+ @failure_count = 0
1341
+ @success_count = 0
1342
+ @last_failure_time = nil
1343
+ @mutex = Mutex.new
1344
+ end
1345
+
1346
+ def call
1347
+ @mutex.synchronize do
1348
+ if open?
1349
+ if timeout_expired?
1350
+ transition_to_half_open
1351
+ else
1352
+ raise CircuitOpenError, "Circuit breaker is open"
1353
+ end
1354
+ end
1355
+ end
1356
+
1357
+ begin
1358
+ result = yield
1359
+ on_success
1360
+ result
1361
+ rescue => e
1362
+ on_failure
1363
+ raise
1364
+ end
1365
+ end
1366
+
1367
+ def open?
1368
+ @state == OPEN
1369
+ end
1370
+
1371
+ def closed?
1372
+ @state == CLOSED
1373
+ end
1374
+
1375
+ def half_open?
1376
+ @state == HALF_OPEN
1377
+ end
1378
+
1379
+ private
1380
+
1381
+ def on_success
1382
+ @mutex.synchronize do
1383
+ @failure_count = 0
1384
+
1385
+ if half_open?
1386
+ @success_count += 1
1387
+
1388
+ if @success_count >= @success_threshold
1389
+ transition_to_closed
1390
+ end
1391
+ end
1392
+ end
1393
+ end
1394
+
1395
+ def on_failure
1396
+ @mutex.synchronize do
1397
+ @failure_count += 1
1398
+ @success_count = 0
1399
+ @last_failure_time = Time.now
1400
+
1401
+ if @failure_count >= @failure_threshold
1402
+ transition_to_open
1403
+ end
1404
+ end
1405
+ end
1406
+
1407
+ def timeout_expired?
1408
+ @last_failure_time && (Time.now - @last_failure_time) >= @timeout
1409
+ end
1410
+
1411
+ def transition_to_open
1412
+ @state = OPEN
1413
+ warn "[E11y] Circuit breaker opened (#{@failure_count} failures)"
1414
+ end
1415
+
1416
+ def transition_to_half_open
1417
+ @state = HALF_OPEN
1418
+ @success_count = 0
1419
+ warn "[E11y] Circuit breaker half-open (testing)"
1420
+ end
1421
+
1422
+ def transition_to_closed
1423
+ @state = CLOSED
1424
+ warn "[E11y] Circuit breaker closed (recovered)"
1425
+ end
1426
+ end
1427
+
1428
+ class CircuitOpenError < StandardError; end
1429
+ end
1430
+ end
1431
+ ```
1432
+
1433
+ ---
1434
+
1435
+ ## 8. Performance & Batching
1436
+
1437
+ ### 8.1. Adaptive Batching
1438
+
1439
+ ```ruby
1440
+ module E11y
1441
+ module Adapters
1442
+ class AdaptiveBatcher
1443
+ def initialize(adapter, min_size: 10, max_size: 1000, timeout: 5.seconds)
1444
+ @adapter = adapter
1445
+ @min_size = min_size
1446
+ @max_size = max_size
1447
+ @timeout = timeout
1448
+
1449
+ @buffer = []
1450
+ @mutex = Mutex.new
1451
+ @last_flush = Time.now
1452
+
1453
+ start_flush_timer!
1454
+ end
1455
+
1456
+ def write(event_data)
1457
+ @mutex.synchronize do
1458
+ @buffer << event_data
1459
+
1460
+ if should_flush?
1461
+ flush!
1462
+ end
1463
+ end
1464
+ end
1465
+
1466
+ def flush!
1467
+ return if @buffer.empty?
1468
+
1469
+ events = @buffer.dup
1470
+ @buffer.clear
1471
+ @last_flush = Time.now
1472
+
1473
+ # Release lock before I/O
1474
+ @mutex.unlock
1475
+
1476
+ begin
1477
+ @adapter.write_batch(events)
1478
+ ensure
1479
+ @mutex.lock
1480
+ end
1481
+ end
1482
+
1483
+ private
1484
+
1485
+ def should_flush?
1486
+ @buffer.size >= @max_size ||
1487
+ (@buffer.size >= @min_size && timeout_expired?)
1488
+ end
1489
+
1490
+ def timeout_expired?
1491
+ (Time.now - @last_flush) >= @timeout
1492
+ end
1493
+
1494
+ def start_flush_timer!
1495
+ Thread.new do
1496
+ loop do
1497
+ sleep(@timeout)
1498
+
1499
+ @mutex.synchronize do
1500
+ flush! if timeout_expired? && @buffer.any?
1501
+ end
1502
+ end
1503
+ end
1504
+ end
1505
+ end
1506
+ end
1507
+ end
1508
+ ```
1509
+
1510
+ ---
1511
+
1512
+ ## 9. Testing Strategy
1513
+
1514
+ ### 9.1. In-Memory Test Adapter
1515
+
1516
+ ```ruby
1517
+ module E11y
1518
+ module Adapters
1519
+ class InMemory < Base
1520
+ attr_reader :events, :batches
1521
+
1522
+ def initialize(config = {})
1523
+ super
1524
+ @events = []
1525
+ @batches = []
1526
+ @mutex = Mutex.new
1527
+ end
1528
+
1529
+ def write(event_data)
1530
+ @mutex.synchronize do
1531
+ @events << event_data
1532
+ end
1533
+ true
1534
+ end
1535
+
1536
+ def write_batch(events)
1537
+ @mutex.synchronize do
1538
+ @events.concat(events)
1539
+ @batches << events
1540
+ end
1541
+ true
1542
+ end
1543
+
1544
+ def clear!
1545
+ @mutex.synchronize do
1546
+ @events.clear
1547
+ @batches.clear
1548
+ end
1549
+ end
1550
+
1551
+ def find_events(pattern)
1552
+ @events.select { |event| event[:event_name].match?(pattern) }
1553
+ end
1554
+
1555
+ def event_count(event_name: nil)
1556
+ if event_name
1557
+ @events.count { |event| event[:event_name] == event_name }
1558
+ else
1559
+ @events.size
1560
+ end
1561
+ end
1562
+ end
1563
+ end
1564
+ end
1565
+ ```
1566
+
1567
+ ### 9.2. RSpec Helpers
1568
+
1569
+ ```ruby
1570
+ RSpec.describe "Adapter Testing" do
1571
+ let(:test_adapter) { E11y::Adapters::InMemory.new }
1572
+
1573
+ before do
1574
+ E11y.configure do |config|
1575
+ config.adapters do
1576
+ register :test, test_adapter
1577
+ end
1578
+ end
1579
+ end
1580
+
1581
+ after do
1582
+ test_adapter.clear!
1583
+ end
1584
+
1585
+ it "tracks events to test adapter" do
1586
+ Events::OrderPaid.track(order_id: '123', amount: 99.99)
1587
+
1588
+ expect(test_adapter.events.size).to eq(1)
1589
+ expect(test_adapter.events.first[:event_name]).to eq('order.paid')
1590
+ end
1591
+
1592
+ it "supports batch writes" do
1593
+ 10.times { |i| Events::OrderPaid.track(order_id: i, amount: 10.0) }
1594
+
1595
+ expect(test_adapter.event_count).to eq(10)
1596
+ expect(test_adapter.batches.size).to be > 0
1597
+ end
1598
+ end
1599
+ ```
1600
+
1601
+ ---
1602
+
1603
+ ## 10. Custom Adapters
1604
+
1605
+ ### 10.1. Creating a Custom Adapter
1606
+
1607
+ ```ruby
1608
+ # lib/adapters/slack_adapter.rb
1609
+ module Adapters
1610
+ class Slack < E11y::Adapters::Base
1611
+ def initialize(config = {})
1612
+ super
1613
+ @webhook_url = config.fetch(:webhook_url)
1614
+ @channel = config.fetch(:channel, '#alerts')
1615
+ @username = config.fetch(:username, 'E11y Bot')
1616
+ end
1617
+
1618
+ def write(event_data)
1619
+ # Only send error+ to Slack
1620
+ return true unless [:error, :fatal].include?(event_data[:severity])
1621
+
1622
+ send_to_slack(event_data)
1623
+ true
1624
+ rescue => e
1625
+ warn "Slack adapter error: #{e.message}"
1626
+ false
1627
+ end
1628
+
1629
+ def capabilities
1630
+ {
1631
+ batching: false,
1632
+ compression: false,
1633
+ async: true,
1634
+ streaming: false
1635
+ }
1636
+ end
1637
+
1638
+ private
1639
+
1640
+ def send_to_slack(event_data)
1641
+ require 'net/http'
1642
+ require 'uri'
1643
+
1644
+ uri = URI.parse(@webhook_url)
1645
+
1646
+ payload = {
1647
+ channel: @channel,
1648
+ username: @username,
1649
+ text: format_message(event_data),
1650
+ attachments: [
1651
+ {
1652
+ color: severity_color(event_data[:severity]),
1653
+ fields: [
1654
+ { title: 'Event', value: event_data[:event_name], short: true },
1655
+ { title: 'Severity', value: event_data[:severity], short: true },
1656
+ { title: 'Trace ID', value: event_data[:trace_id], short: false }
1657
+ ],
1658
+ ts: event_data[:timestamp].to_i
1659
+ }
1660
+ ]
1661
+ }
1662
+
1663
+ http = Net::HTTP.new(uri.host, uri.port)
1664
+ http.use_ssl = true
1665
+
1666
+ request = Net::HTTP::Post.new(uri.path)
1667
+ request['Content-Type'] = 'application/json'
1668
+ request.body = payload.to_json
1669
+
1670
+ http.request(request)
1671
+ end
1672
+
1673
+ def format_message(event_data)
1674
+ "🚨 *#{event_data[:event_name]}*: #{event_data[:message]}"
1675
+ end
1676
+
1677
+ def severity_color(severity)
1678
+ case severity
1679
+ when :error then 'danger'
1680
+ when :fatal then 'danger'
1681
+ when :warn then 'warning'
1682
+ else 'good'
1683
+ end
1684
+ end
1685
+ end
1686
+ end
1687
+
1688
+ # Register in config
1689
+ E11y.configure do |config|
1690
+ config.adapters do
1691
+ register :slack, Adapters::Slack.new(
1692
+ webhook_url: ENV['SLACK_WEBHOOK_URL'],
1693
+ channel: '#production-alerts'
1694
+ )
1695
+ end
1696
+ end
1697
+ ```
1698
+
1699
+ ---
1700
+
1701
+ ## 10a. Event-Level Adapter Configuration (NEW - v1.1)
1702
+
1703
+ > **🎯 CONTRADICTION_01 Resolution:** Move adapter configuration from global initializer to event classes.
1704
+ >
1705
+ > **Goal:** Reduce global config from 1400+ lines to <300 lines by distributing adapter routing across events.
1706
+
1707
+ ### 10a.1. Problem: Global Adapter Routing Complexity
1708
+
1709
+ **Before (v1.0): All routing in global config**
1710
+
1711
+ ```ruby
1712
+ # config/initializers/e11y.rb (1400+ lines!)
1713
+ E11y.configure do |config|
1714
+ # Register adapters (infrastructure)
1715
+ config.register_adapter :loki, Loki.new(url: ENV['LOKI_URL'])
1716
+ config.register_adapter :sentry, Sentry.new(dsn: ENV['SENTRY_DSN'])
1717
+ config.register_adapter :s3, S3Adapter.new(bucket: 'events-archive')
1718
+ config.register_adapter :audit_encrypted, AuditAdapter.new(...)
1719
+
1720
+ # ❌ PROBLEM: Routing for EVERY event in global config
1721
+ config.events do
1722
+ # Payment events → multiple adapters
1723
+ event 'Events::PaymentSucceeded' do
1724
+ adapters [:loki, :sentry, :s3]
1725
+ end
1726
+ event 'Events::PaymentFailed' do
1727
+ adapters [:loki, :sentry, :s3]
1728
+ end
1729
+ event 'Events::PaymentRefunded' do
1730
+ adapters [:loki, :sentry, :s3]
1731
+ end
1732
+
1733
+ # Audit events → encrypted adapter
1734
+ event 'Events::UserDeleted' do
1735
+ adapters [:audit_encrypted]
1736
+ end
1737
+ event 'Events::PermissionChanged' do
1738
+ adapters [:audit_encrypted]
1739
+ end
1740
+
1741
+ # Debug events → file only
1742
+ event 'Events::DebugQuery' do
1743
+ adapters [:file]
1744
+ end
1745
+
1746
+ # ... 100+ more events ...
1747
+ end
1748
+ end
1749
+ ```
1750
+
1751
+ **Problems:**
1752
+ - ❌ 1400+ lines of global config
1753
+ - ❌ Routing far from event definition (locality violation)
1754
+ - ❌ Hard to find which events use which adapters
1755
+ - ❌ Duplication (many events → same adapters)
1756
+
1757
+ ### 10a.2. Solution: Event-Level Adapter Declaration
1758
+
1759
+ **After (v1.1): Adapters in event classes**
1760
+
1761
+ ```ruby
1762
+ # config/initializers/e11y.rb (<300 lines!)
1763
+ E11y.configure do |config|
1764
+ # ONLY infrastructure (adapter registration)
1765
+ config.register_adapter :loki, Loki.new(url: ENV['LOKI_URL'])
1766
+ config.register_adapter :sentry, Sentry.new(dsn: ENV['SENTRY_DSN'])
1767
+ config.register_adapter :s3, S3Adapter.new(bucket: 'events-archive')
1768
+ config.register_adapter :audit_encrypted, AuditAdapter.new(...)
1769
+
1770
+ # Optional: default adapters (convention)
1771
+ config.default_adapters = [:loki] # Most events → Loki
1772
+ end
1773
+
1774
+ # app/events/payment_succeeded.rb
1775
+ module Events
1776
+ class PaymentSucceeded < E11y::Event::Base
1777
+ schema do; required(:transaction_id).filled(:string); end
1778
+
1779
+ # ✅ Adapters right next to schema!
1780
+ adapters [:loki, :sentry, :s3]
1781
+ end
1782
+ end
1783
+
1784
+ # app/events/user_deleted.rb
1785
+ module Events
1786
+ class UserDeleted < E11y::Event::Base
1787
+ schema do; required(:user_id).filled(:string); end
1788
+
1789
+ # ✅ Adapters right next to schema!
1790
+ adapters [:audit_encrypted]
1791
+ end
1792
+ end
1793
+
1794
+ # app/events/debug_query.rb
1795
+ module Events
1796
+ class DebugQuery < E11y::Event::Base
1797
+ schema do; required(:query).filled(:string); end
1798
+
1799
+ # ✅ Adapters right next to schema!
1800
+ adapters [:file]
1801
+ end
1802
+ end
1803
+ ```
1804
+
1805
+ **Benefits:**
1806
+ - ✅ Global config <300 lines (only infrastructure)
1807
+ - ✅ Locality of behavior (adapters next to schema)
1808
+ - ✅ Easy to find (grep event file)
1809
+ - ✅ DRY via inheritance (see below)
1810
+
1811
+ ### 10a.3. Inheritance for Adapter Routing
1812
+
1813
+ **Base class with common adapters:**
1814
+
1815
+ ```ruby
1816
+ # app/events/base_payment_event.rb
1817
+ module Events
1818
+ class BasePaymentEvent < E11y::Event::Base
1819
+ # Common adapters for ALL payment events
1820
+ adapters [:loki, :sentry, :s3]
1821
+
1822
+ # Common config
1823
+ severity :success
1824
+ sample_rate 1.0
1825
+ retention 7.years
1826
+ end
1827
+ end
1828
+
1829
+ # Inherit from base (1-2 lines per event!)
1830
+ class Events::PaymentSucceeded < Events::BasePaymentEvent
1831
+ schema do; required(:transaction_id).filled(:string); end
1832
+ # ← Inherits: adapters [:loki, :sentry, :s3]
1833
+ end
1834
+
1835
+ class Events::PaymentFailed < Events::BasePaymentEvent
1836
+ severity :error # ← Override severity
1837
+ schema do; required(:error_code).filled(:string); end
1838
+ # ← Inherits: adapters [:loki, :sentry, :s3]
1839
+ end
1840
+
1841
+ class Events::PaymentRefunded < Events::BasePaymentEvent
1842
+ schema do; required(:refund_id).filled(:string); end
1843
+ # ← Inherits: adapters [:loki, :sentry, :s3]
1844
+ end
1845
+ ```
1846
+
1847
+ **Base class for audit events:**
1848
+
1849
+ ```ruby
1850
+ # app/events/base_audit_event.rb
1851
+ module Events
1852
+ class BaseAuditEvent < E11y::Event::Base
1853
+ # Common adapters for ALL audit events
1854
+ adapters [:audit_encrypted]
1855
+
1856
+ # Common config
1857
+ audit_event true
1858
+ # ← Auto-set: retention = E11y.config.audit_retention (configurable!)
1859
+ # rate_limiting = false (LOCKED!)
1860
+ # sampling = false (LOCKED!)
1861
+ end
1862
+ end
1863
+
1864
+ # Inherit from base
1865
+ class Events::UserDeleted < Events::BaseAuditEvent
1866
+ schema do; required(:user_id).filled(:string); end
1867
+ # ← Inherits: adapters [:audit_encrypted]
1868
+ end
1869
+
1870
+ class Events::PermissionChanged < Events::BaseAuditEvent
1871
+ schema do; required(:user_id).filled(:string); end
1872
+ # ← Inherits: adapters [:audit_encrypted]
1873
+ end
1874
+ ```
1875
+
1876
+ ### 10a.4. Preset Modules for Adapters
1877
+
1878
+ ```ruby
1879
+ # lib/e11y/presets/high_value_event.rb
1880
+ module E11y
1881
+ module Presets
1882
+ module HighValueEvent
1883
+ extend ActiveSupport::Concern
1884
+ included do
1885
+ adapters [:loki, :sentry, :s3_archive]
1886
+ sample_rate 1.0
1887
+ retention 7.years
1888
+ end
1889
+ end
1890
+
1891
+ module DebugEvent
1892
+ extend ActiveSupport::Concern
1893
+ included do
1894
+ adapters [:file] # Local only
1895
+ severity :debug
1896
+ sample_rate 0.01
1897
+ retention 7.days
1898
+ end
1899
+ end
1900
+ end
1901
+ end
1902
+
1903
+ # Usage:
1904
+ class Events::PaymentProcessed < E11y::Event::Base
1905
+ include E11y::Presets::HighValueEvent # ← Adapters inherited!
1906
+ schema do; required(:transaction_id).filled(:string); end
1907
+ end
1908
+
1909
+ class Events::DebugQuery < E11y::Event::Base
1910
+ include E11y::Presets::DebugEvent # ← Adapters inherited!
1911
+ schema do; required(:query).filled(:string); end
1912
+ end
1913
+ ```
1914
+
1915
+ ### 10a.5. Conventions for Adapters (Sensible Defaults)
1916
+
1917
+ ```ruby
1918
+ # Convention: Severity → Adapters
1919
+ # :error/:fatal → [:sentry]
1920
+ # :success/:info/:warn → [:loki]
1921
+ # :debug → [:file] (dev), [:loki] (prod with sampling)
1922
+
1923
+ # Zero-config event (uses conventions):
1924
+ class Events::OrderCreated < E11y::Event::Base
1925
+ severity :success
1926
+ schema do; required(:order_id).filled(:string); end
1927
+ # ← Auto: adapters = [:loki] (from severity!)
1928
+ end
1929
+
1930
+ class Events::CriticalError < E11y::Event::Base
1931
+ severity :fatal
1932
+ schema do; required(:error_code).filled(:string); end
1933
+ # ← Auto: adapters = [:sentry] (from severity!)
1934
+ end
1935
+
1936
+ # Override convention:
1937
+ class Events::OrderCreated < E11y::Event::Base
1938
+ severity :success
1939
+ adapters [:loki, :elasticsearch, :s3] # ← Override
1940
+ schema do; required(:order_id).filled(:string); end
1941
+ end
1942
+ ```
1943
+
1944
+ ### 10a.6. Adapter Strategy: Replace vs Append
1945
+
1946
+ ```ruby
1947
+ # Replace strategy (default): Override parent/convention
1948
+ class Events::PaymentSucceeded < Events::BasePaymentEvent
1949
+ adapters [:loki, :sentry] # ← Replaces base [:loki, :sentry, :s3]
1950
+ end
1951
+
1952
+ # Append strategy: Add to parent/convention
1953
+ class Events::PaymentSucceeded < Events::BasePaymentEvent
1954
+ adapters_strategy :append
1955
+ adapters [:slack_business] # ← Adds to base (result: [:loki, :sentry, :s3, :slack_business])
1956
+ end
1957
+ ```
1958
+
1959
+ ### 10a.7. Precedence Rules
1960
+
1961
+ ```ruby
1962
+ # Precedence (highest to lowest):
1963
+ # 1. Event-level explicit: adapters [:loki]
1964
+ # 2. Preset module: include E11y::Presets::HighValueEvent
1965
+ # 3. Base class inheritance: class < BasePaymentEvent
1966
+ # 4. Convention: severity :success → [:loki]
1967
+ # 5. Global default: config.default_adapters = [:loki]
1968
+
1969
+ # Example:
1970
+ E11y.configure do |config|
1971
+ config.default_adapters = [:loki] # 5. Global default
1972
+ end
1973
+
1974
+ class Events::BasePaymentEvent < E11y::Event::Base
1975
+ adapters [:loki, :sentry] # 3. Base class
1976
+ end
1977
+
1978
+ class Events::PaymentSucceeded < Events::BasePaymentEvent
1979
+ include E11y::Presets::HighValueEvent # 2. Preset (adds :s3)
1980
+ adapters [:loki, :sentry, :pagerduty] # 1. Event-level (WINS!)
1981
+ end
1982
+
1983
+ # Result: [:loki, :sentry, :pagerduty]
1984
+ ```
1985
+
1986
+ ### 10a.8. Migration Path
1987
+
1988
+ **Step 1: Keep global config (backward compatible)**
1989
+
1990
+ ```ruby
1991
+ # v1.1 supports BOTH global and event-level config
1992
+ E11y.configure do |config|
1993
+ # Old style (still works):
1994
+ config.events do
1995
+ event 'Events::OrderCreated' do
1996
+ adapters [:loki]
1997
+ end
1998
+ end
1999
+ end
2000
+
2001
+ # New style (preferred):
2002
+ class Events::OrderCreated < E11y::Event::Base
2003
+ adapters [:loki] # ← Event-level (overrides global)
2004
+ end
2005
+ ```
2006
+
2007
+ **Step 2: Migrate incrementally**
2008
+
2009
+ ```ruby
2010
+ # Migrate high-value events first:
2011
+ class Events::PaymentSucceeded < E11y::Event::Base
2012
+ adapters [:loki, :sentry, :s3] # ← Migrated
2013
+ end
2014
+
2015
+ # Keep others in global config (temporary):
2016
+ E11y.configure do |config|
2017
+ config.events do
2018
+ event 'Events::DebugQuery' do
2019
+ adapters [:file] # ← Not migrated yet
2020
+ end
2021
+ end
2022
+ end
2023
+ ```
2024
+
2025
+ **Step 3: Remove global routing**
2026
+
2027
+ ```ruby
2028
+ # After all events migrated:
2029
+ E11y.configure do |config|
2030
+ # Only infrastructure (adapters registration)
2031
+ config.register_adapter :loki, Loki.new(...)
2032
+ config.register_adapter :sentry, Sentry.new(...)
2033
+
2034
+ # Optional: default adapters (convention)
2035
+ config.default_adapters = [:loki]
2036
+
2037
+ # ✅ No more config.events { ... } block!
2038
+ end
2039
+ ```
2040
+
2041
+ ### 10a.9. Benefits Summary
2042
+
2043
+ | Aspect | Before (v1.0) | After (v1.1) | Improvement |
2044
+ |--------|---------------|--------------|-------------|
2045
+ | **Global config lines** | 1400+ | <300 | 78% reduction |
2046
+ | **Locality** | ❌ Far from event | ✅ Next to schema | Better |
2047
+ | **DRY** | ❌ Duplication | ✅ Inheritance | Better |
2048
+ | **Discoverability** | ❌ Hard to find | ✅ Grep event file | Better |
2049
+ | **Conventions** | ❌ None | ✅ Severity → adapters | Better |
2050
+ | **Backward compat** | N/A | ✅ Both styles work | Safe |
2051
+
2052
+ ---
2053
+
2054
+ ## 11. Trade-offs
2055
+
2056
+ ### 11.1. Key Decisions
2057
+
2058
+ | Decision | Pro | Con | Rationale |
2059
+ |----------|-----|-----|-----------|
2060
+ | **Global registry** | Configure once, reuse | Less flexibility | DRY principle |
2061
+ | **Batching by default** | Better performance | Slight latency | 99% use cases benefit |
2062
+ | **Connection pooling** | Resource efficient | Memory overhead | Critical for scale |
2063
+ | **Circuit breaker** | Prevents cascades | Complexity | Reliability critical |
2064
+ | **Sync interface** | Simple to reason | Blocks thread | Buffering mitigates |
2065
+
2066
+ ### 11.2. Alternatives Considered
2067
+
2068
+ **A) No adapter abstraction**
2069
+ - ❌ Rejected: Every event hardcodes destinations
2070
+
2071
+ **B) Adapter instances in events**
2072
+ - ❌ Rejected: Duplication, no connection reuse
2073
+
2074
+ **C) Async by default**
2075
+ - ❌ Rejected: Complexity, hard to test
2076
+
2077
+ ---
2078
+
2079
+ ## 12. Validations & Environment-Specific Configuration (NEW - v1.1)
2080
+
2081
+ ### 12.1. Adapter Registration Validation
2082
+
2083
+ **Problem:** Typos in adapter names → events lost.
2084
+
2085
+ **Solution:** Validate adapters against registered list:
2086
+
2087
+ ```ruby
2088
+ # Gem implementation (automatic):
2089
+ def self.adapters(list)
2090
+ list.each do |adapter|
2091
+ unless E11y.registered_adapters.include?(adapter)
2092
+ raise ArgumentError, "Unknown adapter: #{adapter}. Registered: #{E11y.registered_adapters.keys.join(', ')}"
2093
+ end
2094
+ end
2095
+ self._adapters = list
2096
+ end
2097
+
2098
+ # Result:
2099
+ class Events::OrderPaid < E11y::Event::Base
2100
+ adapters [:loki, :sentri] # ← ERROR: "Unknown adapter: :sentri. Registered: loki, sentry, file"
2101
+ end
2102
+ ```
2103
+
2104
+ ### 12.2. Environment-Specific Adapters
2105
+
2106
+ **Problem:** Different adapters per environment (dev/staging/prod).
2107
+
2108
+ **Solution:** Use Ruby conditionals:
2109
+
2110
+ ```ruby
2111
+ class Events::DebugQuery < E11y::Event::Base
2112
+ schema do
2113
+ required(:query).filled(:string)
2114
+ end
2115
+
2116
+ # Environment-specific adapters
2117
+ adapters Rails.env.production? ? [:loki] : [:file]
2118
+ end
2119
+
2120
+ class Events::PaymentFailed < E11y::Event::Base
2121
+ schema do
2122
+ required(:order_id).filled(:string)
2123
+ end
2124
+
2125
+ # Multi-environment routing
2126
+ adapters case Rails.env
2127
+ when 'production' then [:loki, :sentry, :s3_archive]
2128
+ when 'staging' then [:loki, :sentry]
2129
+ else [:file]
2130
+ end
2131
+ end
2132
+ ```
2133
+
2134
+ ### 12.3. Precedence Rules for Adapters
2135
+
2136
+ **Precedence Order (Highest to Lowest):**
2137
+
2138
+ ```
2139
+ 1. Event-level explicit config (highest)
2140
+
2141
+ 2. Preset module config
2142
+
2143
+ 3. Base class config (inheritance)
2144
+
2145
+ 4. Convention-based defaults (severity → adapters)
2146
+
2147
+ 5. Global config (lowest)
2148
+ ```
2149
+
2150
+ **Example:**
2151
+
2152
+ ```ruby
2153
+ # Global config (lowest)
2154
+ E11y.configure do |config|
2155
+ config.adapters = [:file] # Default
2156
+ end
2157
+
2158
+ # Base class (medium)
2159
+ class Events::BasePaymentEvent < E11y::Event::Base
2160
+ adapters [:loki, :sentry] # Override global
2161
+ end
2162
+
2163
+ # Preset (higher)
2164
+ module E11y::Presets::HighValueEvent
2165
+ extend ActiveSupport::Concern
2166
+ included do
2167
+ adapters [:loki, :sentry, :s3_archive] # Add S3
2168
+ end
2169
+ end
2170
+
2171
+ # Event (highest)
2172
+ class Events::CriticalPayment < Events::BasePaymentEvent
2173
+ include E11y::Presets::HighValueEvent
2174
+
2175
+ adapters [:loki, :sentry, :s3_archive, :datadog] # Add Datadog
2176
+
2177
+ # Final: [:loki, :sentry, :s3_archive, :datadog] (event-level wins)
2178
+ end
2179
+ ```
2180
+
2181
+ ---
2182
+
2183
+ ## 13. Implementation Examples (2026-01-19)
2184
+
2185
+ ### 13.1. Production Setup with All Adapters
2186
+
2187
+ ```ruby
2188
+ # config/initializers/e11y.rb
2189
+ require 'e11y'
2190
+
2191
+ E11y.configure do |config|
2192
+ # Register Loki for centralized logging
2193
+ E11y::Adapters::Registry.register(
2194
+ :loki,
2195
+ E11y::Adapters::Loki.new(
2196
+ url: ENV['LOKI_URL'] || 'http://loki:3100',
2197
+ labels: {
2198
+ app: 'my_app',
2199
+ env: Rails.env,
2200
+ host: Socket.gethostname
2201
+ },
2202
+ batch_size: 100,
2203
+ batch_timeout: 5,
2204
+ compress: true,
2205
+ tenant_id: ENV['LOKI_TENANT_ID']
2206
+ )
2207
+ )
2208
+
2209
+ # Register Sentry for error tracking
2210
+ E11y::Adapters::Registry.register(
2211
+ :sentry,
2212
+ E11y::Adapters::Sentry.new(
2213
+ dsn: ENV['SENTRY_DSN'],
2214
+ environment: Rails.env,
2215
+ severity_threshold: :warn,
2216
+ breadcrumbs: true
2217
+ )
2218
+ )
2219
+
2220
+ # Register File adapter for local development
2221
+ E11y::Adapters::Registry.register(
2222
+ :file,
2223
+ E11y::Adapters::File.new(
2224
+ path: Rails.root.join('log', 'e11y.log'),
2225
+ rotation: :daily,
2226
+ max_size: 100 * 1024 * 1024, # 100MB
2227
+ compress: true
2228
+ )
2229
+ )
2230
+
2231
+ # Register Stdout for console output
2232
+ E11y::Adapters::Registry.register(
2233
+ :stdout,
2234
+ E11y::Adapters::Stdout.new(
2235
+ pretty: Rails.env.development?,
2236
+ colorize: true
2237
+ )
2238
+ )
2239
+
2240
+ # Register InMemory for testing
2241
+ if Rails.env.test?
2242
+ E11y::Adapters::Registry.register(
2243
+ :test,
2244
+ E11y::Adapters::InMemory.new(
2245
+ max_events: 1000,
2246
+ max_batches: 100
2247
+ )
2248
+ )
2249
+ end
2250
+ end
2251
+ ```
2252
+
2253
+ ### 13.2. Event Examples with Adapters
2254
+
2255
+ ```ruby
2256
+ # Business event - goes to Loki
2257
+ class Events::OrderPlaced < E11y::Event::Base
2258
+ schema do
2259
+ required(:order_id).filled(:string)
2260
+ required(:amount).filled(:integer)
2261
+ required(:currency).filled(:string)
2262
+ end
2263
+
2264
+ severity :info
2265
+ adapters [:loki]
2266
+ end
2267
+
2268
+ # Error event - goes to Sentry and Loki
2269
+ class Events::PaymentFailed < E11y::Event::Base
2270
+ schema do
2271
+ required(:order_id).filled(:string)
2272
+ required(:error_message).filled(:string)
2273
+ optional(:exception).filled
2274
+ end
2275
+
2276
+ severity :error
2277
+ adapters [:sentry, :loki]
2278
+ end
2279
+
2280
+ # Debug event - only in development
2281
+ class Events::DebugQuery < E11y::Event::Base
2282
+ schema do
2283
+ required(:query).filled(:string)
2284
+ required(:duration_ms).filled(:integer)
2285
+ end
2286
+
2287
+ severity :debug
2288
+ adapters Rails.env.development? ? [:stdout] : []
2289
+ end
2290
+
2291
+ # Usage
2292
+ Events::OrderPlaced.track(
2293
+ order_id: 'ORD-123',
2294
+ amount: 10000,
2295
+ currency: 'USD'
2296
+ )
2297
+
2298
+ Events::PaymentFailed.track(
2299
+ order_id: 'ORD-456',
2300
+ error_message: 'Card declined',
2301
+ exception: StandardError.new('Payment gateway error')
2302
+ )
2303
+ ```
2304
+
2305
+ ### 13.3. Testing with InMemory Adapter
2306
+
2307
+ ```ruby
2308
+ # spec/events/order_placed_spec.rb
2309
+ RSpec.describe Events::OrderPlaced do
2310
+ let(:adapter) { E11y::Adapters::Registry.resolve(:test) }
2311
+
2312
+ before do
2313
+ adapter.clear!
2314
+ end
2315
+
2316
+ it 'tracks order placement' do
2317
+ described_class.track(
2318
+ order_id: 'ORD-123',
2319
+ amount: 10000,
2320
+ currency: 'USD'
2321
+ )
2322
+
2323
+ expect(adapter.event_count).to eq(1)
2324
+
2325
+ event = adapter.events.first
2326
+ expect(event[:event_name]).to eq('order.placed')
2327
+ expect(event[:order_id]).to eq('ORD-123')
2328
+ expect(event[:amount]).to eq(10000)
2329
+ end
2330
+
2331
+ it 'finds events by pattern' do
2332
+ Events::OrderPlaced.track(order_id: 'ORD-1', amount: 100, currency: 'USD')
2333
+ Events::OrderPlaced.track(order_id: 'ORD-2', amount: 200, currency: 'USD')
2334
+
2335
+ order_events = adapter.find_events(/order\.placed/)
2336
+ expect(order_events.size).to eq(2)
2337
+ end
2338
+ end
2339
+ ```
2340
+
2341
+ ### 13.4. Adapter Capabilities Check
2342
+
2343
+ ```ruby
2344
+ # Check adapter capabilities before using
2345
+ loki = E11y::Adapters::Registry.resolve(:loki)
2346
+ puts loki.capabilities
2347
+ # => {
2348
+ # batching: true,
2349
+ # compression: true,
2350
+ # async: true,
2351
+ # streaming: false
2352
+ # }
2353
+
2354
+ # Use batching if supported
2355
+ if loki.capabilities[:batching]
2356
+ events = [event1, event2, event3]
2357
+ loki.write_batch(events)
2358
+ else
2359
+ events.each { |e| loki.write(e) }
2360
+ end
2361
+ ```
2362
+
2363
+ ### 13.5. Health Checks
2364
+
2365
+ ```ruby
2366
+ # config/initializers/health_check.rb
2367
+ HealthCheck.setup do |config|
2368
+ config.add_custom_check('e11y_adapters') do
2369
+ adapters = E11y::Adapters::Registry.all
2370
+ unhealthy = adapters.reject { |name, adapter| adapter.healthy? }
2371
+
2372
+ if unhealthy.any?
2373
+ "Unhealthy adapters: #{unhealthy.keys.join(', ')}"
2374
+ else
2375
+ ''
2376
+ end
2377
+ end
2378
+ end
2379
+ ```
2380
+
2381
+ ---
2382
+
2383
+ **Status:** ✅ Draft Complete (Updated to Unified DSL v1.1.0)
2384
+ **Next:** ADR-006 (Security & Compliance) or ADR-008 (Rails Integration)
2385
+ **Estimated Implementation:** 2 weeks