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,2166 @@
1
+ # ADR-010: Developer Experience
2
+
3
+ **Status:** Draft
4
+ **Date:** January 12, 2026
5
+ **Covers:** UC-017 (Local Development), UC-022 (Event Registry)
6
+ **Depends On:** ADR-001 (Core), ADR-011 (Testing), ADR-012 (Event Evolution)
7
+
8
+ ---
9
+
10
+ ## 📋 Table of Contents
11
+
12
+ 1. [Context & Problem](#1-context--problem)
13
+ 2. [Architecture Overview](#2-architecture-overview)
14
+ 3. [Console Output](#3-console-output)
15
+ 4. [Web UI](#4-web-ui)
16
+ 5. [Event Registry](#5-event-registry)
17
+ 6. [Debug Helpers](#6-debug-helpers)
18
+ 7. [CLI Tools](#7-cli-tools)
19
+ 8. [Documentation Generation](#8-documentation-generation)
20
+ 9. [Trade-offs](#9-trade-offs)
21
+
22
+ ---
23
+
24
+ ## 1. Context & Problem
25
+
26
+ ### 1.1. Problem Statement
27
+
28
+ **Current Pain Points:**
29
+
30
+ 1. **Poor Local Development:**
31
+ ```ruby
32
+ # ❌ No visibility into tracked events
33
+ Events::OrderCreated.track(order_id: 123)
34
+ # Where did this event go? 🤷
35
+ # Check logs? Database? Nothing!
36
+ ```
37
+
38
+ 2. **No Event Discovery:**
39
+ ```ruby
40
+ # ❌ How do I know what events exist?
41
+ # What's the schema for OrderCreated?
42
+ # Which events are deprecated?
43
+ ```
44
+
45
+ 3. **Debug Difficulty:**
46
+ ```ruby
47
+ # ❌ Can't inspect event pipeline
48
+ # What adapters received the event?
49
+ # Was it sampled? Filtered?
50
+ ```
51
+
52
+ 4. **No Visual Tools:**
53
+ ```ruby
54
+ # ❌ Command-line only
55
+ # No UI to browse events
56
+ # No timeline view
57
+ ```
58
+
59
+ ### 1.1.1. Architecture Decision: File-Based Event Store (JSONL)
60
+
61
+ **Key Insight:** Web UI runs **ONLY in development/test**, using structured JSONL log file:
62
+
63
+ ```ruby
64
+ # Development: Events stored in JSONL file (Web UI accessible)
65
+ # Production: NO dev log, NO Web UI
66
+
67
+ # config/environments/development.rb
68
+ E11y.configure do |config|
69
+ # ✅ File-based adapter for Web UI (dev only!)
70
+ config.adapters.register :dev_log, E11y::Adapters::DevLog.new(
71
+ path: Rails.root.join('log', 'e11y_dev.jsonl'),
72
+ max_lines: 10_000, # Keep last 10K events
73
+ max_size: 10.megabytes, # Auto-rotate at 10MB
74
+ enable_watcher: true # File watching for near-realtime
75
+ )
76
+
77
+ # Console adapter for immediate feedback
78
+ config.adapters.register :console, E11y::Adapters::Console.new(
79
+ colorize: true,
80
+ pretty_print: true
81
+ )
82
+ end
83
+
84
+ # config/environments/production.rb
85
+ E11y.configure do |config|
86
+ # ❌ NO dev_log adapter in production!
87
+ # ❌ NO Web UI in production!
88
+
89
+ # Real adapters only
90
+ config.adapters.register :loki, E11y::Adapters::Loki.new(...)
91
+ config.adapters.register :sentry, E11y::Adapters::Sentry.new(...)
92
+ end
93
+ ```
94
+
95
+ **Why File-Based (JSONL)?**
96
+
97
+ | Approach | Pros | Cons | Decision |
98
+ |----------|------|------|----------|
99
+ | **A) Query Loki/ES** | Real data | Slow, requires infrastructure | ❌ Too complex |
100
+ | **B) Store in Redis** | Fast, real-time | Requires Redis in dev | ❌ Extra dependency |
101
+ | **C) Store in DB** | Persistent | Pollutes DB | ❌ Not isolated |
102
+ | **D) In-Memory** | Fast | **Broken for multi-process** | ❌ Doesn't work |
103
+ | **E) File (JSONL)** | Multi-process, zero deps, persistent | Read overhead (~50ms) | ✅ **CHOSEN** |
104
+
105
+ **Why JSONL format?**
106
+ - ✅ **Multi-process safe** (append-only writes)
107
+ - ✅ **Zero dependencies** (just filesystem)
108
+ - ✅ **Persistence** (survives restarts)
109
+ - ✅ **Greppable**: `tail -f log/e11y_dev.jsonl | jq`
110
+ - ✅ **Near-realtime** (3-second polling)
111
+ - ✅ **Auto-rotation** (won't grow infinitely)
112
+
113
+ **Trade-off:** ~50ms read latency vs zero dependencies + multi-process support.
114
+
115
+ ### 1.2. Goals
116
+
117
+ **Primary Goals:**
118
+ - ✅ **Beautiful console output** (colored, pretty-printed)
119
+ - ✅ **Web UI** for event exploration
120
+ - ✅ **Event Registry** API for introspection
121
+ - ✅ **Debug helpers** (E11y.stats, E11y.inspect)
122
+ - ✅ **CLI tools** (rake tasks)
123
+ - ✅ **Auto-generated docs** from event schemas
124
+
125
+ **Non-Goals:**
126
+ - ❌ Production-grade Web UI (dev only)
127
+ - ❌ Complex event filtering (v1.0 basic)
128
+ - ❌ Real-time event streaming
129
+
130
+ ### 1.3. Success Metrics
131
+
132
+ | Metric | Target | Critical? |
133
+ |--------|--------|-----------|
134
+ | **Setup time** | <1 minute | ✅ Yes |
135
+ | **Event discovery** | 100% visible | ✅ Yes |
136
+ | **Debug speed** | <30 seconds | ✅ Yes |
137
+ | **Documentation coverage** | 100% auto-generated | ✅ Yes |
138
+
139
+ ---
140
+
141
+ ## 2. Architecture Overview
142
+
143
+ ### 2.1. System Context
144
+
145
+ ```mermaid
146
+ C4Context
147
+ title Developer Experience Context
148
+
149
+ Person(dev, "Developer", "Local development")
150
+
151
+ System(rails_app, "Rails App", "Tracks events")
152
+
153
+ System(e11y, "E11y Gem", "Developer tools")
154
+
155
+ System_Ext(console, "Console Output", "Colored, pretty-printed")
156
+ System_Ext(web_ui, "Web UI", "Event explorer")
157
+ System_Ext(registry, "Event Registry", "Introspection API")
158
+ System_Ext(docs, "Auto-Generated Docs", "Schema documentation")
159
+
160
+ Rel(dev, rails_app, "Develops", "Code")
161
+ Rel(rails_app, e11y, "Tracks events", "E11y API")
162
+ Rel(e11y, console, "Pretty prints", "Stdout")
163
+ Rel(dev, web_ui, "Views events", "Browser")
164
+ Rel(e11y, web_ui, "Provides data", "HTTP")
165
+ Rel(dev, registry, "Queries", "Ruby API")
166
+ Rel(e11y, docs, "Generates", "Markdown")
167
+ ```
168
+
169
+ ### 2.2. Component Architecture (Development Environment)
170
+
171
+ ```mermaid
172
+ graph TB
173
+ subgraph "Event Tracking (Development)"
174
+ App[Rails App] --> Track[Events::OrderCreated.track]
175
+ Track --> Pipeline[E11y Pipeline]
176
+
177
+ Pipeline --> ConsoleAdapter[Console Adapter<br/>stdout]
178
+ Pipeline --> MemoryAdapter[In-Memory Adapter<br/>RAM, 1000 events, 1h TTL]
179
+ Pipeline --> FileAdapter[File Adapter<br/>log/e11y.log]
180
+ end
181
+
182
+ subgraph "Developer Tools"
183
+ MemoryAdapter --> WebUI[Web UI<br/>http://localhost:3000/e11y]
184
+
185
+ WebUI --> EventExplorer[Event Explorer<br/>Browse/Filter/Search]
186
+ WebUI --> Timeline[Timeline View<br/>Chronological]
187
+ WebUI --> Inspector[Event Inspector<br/>Detailed view]
188
+ WebUI --> TraceView[Trace Viewer<br/>Group by trace_id]
189
+
190
+ Registry[Event Registry] --> Discovery[Event Discovery<br/>E11y.events]
191
+ Registry --> Schema[Schema Inspector<br/>E11y.inspect]
192
+ Registry --> Deprecation[Deprecation Tracker]
193
+ end
194
+
195
+ subgraph "Debug Helpers"
196
+ DebugAPI[E11y.stats] --> MemoryAdapter
197
+ DebugInspect[E11y.inspect] --> Registry
198
+ DebugTrace[E11y.memory_adapter] --> MemoryAdapter
199
+ end
200
+
201
+ subgraph "CLI Tools"
202
+ RakeTasks[Rake Tasks] --> EventsList[rake e11y:list]
203
+ RakeTasks --> EventsValidate[rake e11y:validate]
204
+ RakeTasks --> EventsDocs[rake e11y:docs:generate]
205
+ RakeTasks --> EventsStats[rake e11y:stats]
206
+ end
207
+
208
+ subgraph "Production (NO Web UI)"
209
+ ProdApp[Rails App] --> ProdPipeline[E11y Pipeline]
210
+ ProdPipeline --> Loki[Loki Adapter]
211
+ ProdPipeline --> Sentry[Sentry Adapter]
212
+
213
+ Note1[❌ NO In-Memory Adapter<br/>❌ NO Web UI<br/>✅ Only Real Adapters]
214
+ end
215
+
216
+ style MemoryAdapter fill:#fff3cd
217
+ style WebUI fill:#d4edda
218
+ style ConsoleAdapter fill:#d1ecf1
219
+ style Note1 fill:#f8d7da
220
+ ```
221
+
222
+ ---
223
+
224
+ ### 2.3. Data Flow: Development vs Production
225
+
226
+ ```mermaid
227
+ sequenceDiagram
228
+ participant App as Rails App
229
+ participant E11y as E11y Pipeline
230
+ participant Memory as InMemory Adapter
231
+ participant Console as Console Adapter
232
+ participant WebUI as Web UI (Browser)
233
+ participant Loki as Loki (Production)
234
+
235
+ Note over App,Loki: DEVELOPMENT
236
+
237
+ App->>E11y: Events::OrderCreated.track(...)
238
+
239
+ E11y->>Console: send_batch (stdout)
240
+ Console-->>App: ✓ printed to terminal
241
+
242
+ E11y->>Memory: send_batch (store in RAM)
243
+ Memory-->>Memory: events = [event1, event2, ...]
244
+ Memory-->>E11y: ✓ stored
245
+
246
+ WebUI->>Memory: GET /e11y/events
247
+ Memory-->>WebUI: all_events (last 1000)
248
+ WebUI-->>WebUI: Render UI
249
+
250
+ Note over App,Loki: PRODUCTION
251
+
252
+ App->>E11y: Events::OrderCreated.track(...)
253
+
254
+ Note over Memory: ❌ Not registered
255
+
256
+ E11y->>Loki: send_batch (HTTP)
257
+ Loki-->>E11y: ✓ sent
258
+
259
+ Note over WebUI: ❌ Not mounted
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 3. Console Output
265
+
266
+ ### 3.1. Pretty Console Adapter
267
+
268
+ ```ruby
269
+ # lib/e11y/adapters/console.rb
270
+ module E11y
271
+ module Adapters
272
+ class Console < Base
273
+ def initialize(config = {})
274
+ super(name: :console)
275
+ @colorize = config[:colorize] != false
276
+ @pretty_print = config[:pretty_print] != false
277
+ @show_payload = config[:show_payload] != false
278
+ @show_metadata = config[:show_metadata] || false
279
+ end
280
+
281
+ def send_batch(events)
282
+ events.each do |event|
283
+ print_event(event)
284
+ end
285
+
286
+ { success: true, sent: events.size }
287
+ end
288
+
289
+ private
290
+
291
+ def print_event(event)
292
+ output = []
293
+
294
+ # Header with timestamp and severity
295
+ output << format_header(event)
296
+
297
+ # Event name
298
+ output << format_event_name(event)
299
+
300
+ # Payload (if enabled)
301
+ if @show_payload
302
+ output << format_payload(event[:payload])
303
+ end
304
+
305
+ # Metadata (if enabled)
306
+ if @show_metadata
307
+ output << format_metadata(event)
308
+ end
309
+
310
+ # Separator
311
+ output << colorize('─' * 80, :gray)
312
+
313
+ puts output.join("\n")
314
+ end
315
+
316
+ def format_header(event)
317
+ timestamp = Time.parse(event[:timestamp]).strftime('%H:%M:%S.%L')
318
+ severity = event[:severity].to_s.upcase.ljust(8)
319
+
320
+ colorize("#{timestamp} #{severity}", severity_color(event[:severity]))
321
+ end
322
+
323
+ def format_event_name(event)
324
+ event_name = event[:event_name]
325
+
326
+ " #{colorize('→', :cyan)} #{colorize(event_name, :white, :bold)}"
327
+ end
328
+
329
+ def format_payload(payload)
330
+ return '' if payload.empty?
331
+
332
+ lines = []
333
+ lines << " #{colorize('Payload:', :yellow)}"
334
+
335
+ payload.each do |key, value|
336
+ formatted_value = format_value(value)
337
+ lines << " #{colorize(key.to_s, :green)}: #{formatted_value}"
338
+ end
339
+
340
+ lines.join("\n")
341
+ end
342
+
343
+ def format_metadata(event)
344
+ lines = []
345
+ lines << " #{colorize('Metadata:', :yellow)}"
346
+
347
+ metadata = {
348
+ trace_id: event[:trace_id],
349
+ span_id: event[:span_id],
350
+ adapters: event[:adapters]&.join(', ')
351
+ }.compact
352
+
353
+ metadata.each do |key, value|
354
+ lines << " #{colorize(key.to_s, :magenta)}: #{value}"
355
+ end
356
+
357
+ lines.join("\n")
358
+ end
359
+
360
+ def format_value(value)
361
+ case value
362
+ when String
363
+ colorize("\"#{truncate(value, 50)}\"", :cyan)
364
+ when Numeric
365
+ colorize(value.to_s, :blue)
366
+ when TrueClass, FalseClass
367
+ colorize(value.to_s, :yellow)
368
+ when NilClass
369
+ colorize('nil', :gray)
370
+ when Array
371
+ colorize("[#{value.size} items]", :magenta)
372
+ when Hash
373
+ colorize("{#{value.size} keys}", :magenta)
374
+ else
375
+ value.to_s
376
+ end
377
+ end
378
+
379
+ def severity_color(severity)
380
+ case severity.to_sym
381
+ when :debug then :gray
382
+ when :info then :white
383
+ when :success then :green
384
+ when :warn then :yellow
385
+ when :error then :red
386
+ when :fatal then :red
387
+ else :white
388
+ end
389
+ end
390
+
391
+ def colorize(text, color, style = nil)
392
+ return text unless @colorize
393
+
394
+ codes = []
395
+
396
+ # Colors
397
+ codes << case color
398
+ when :gray then 90
399
+ when :red then 31
400
+ when :green then 32
401
+ when :yellow then 33
402
+ when :blue then 34
403
+ when :magenta then 35
404
+ when :cyan then 36
405
+ when :white then 37
406
+ else 0
407
+ end
408
+
409
+ # Styles
410
+ codes << 1 if style == :bold
411
+ codes << 4 if style == :underline
412
+
413
+ "\e[#{codes.join(';')}m#{text}\e[0m"
414
+ end
415
+
416
+ def truncate(string, length)
417
+ string.length > length ? "#{string[0...length]}..." : string
418
+ end
419
+ end
420
+ end
421
+ end
422
+ ```
423
+
424
+ ### 3.2. Example Output
425
+
426
+ ```
427
+ 18:42:31.123 INFO
428
+ → Events::OrderCreated
429
+ Payload:
430
+ order_id: 12345
431
+ user_id: 678
432
+ amount: 99.99
433
+ currency: "USD"
434
+ Metadata:
435
+ trace_id: 0af7651916cd43dd8448eb211c80319c
436
+ adapters: loki, sentry
437
+ ────────────────────────────────────────────────────────────────────────────────
438
+ ```
439
+
440
+ ### 3.3. Configuration
441
+
442
+ ```ruby
443
+ # config/environments/development.rb
444
+ E11y.configure do |config|
445
+ config.adapters.register :console, E11y::Adapters::Console.new(
446
+ colorize: true,
447
+ pretty_print: true,
448
+ show_payload: true,
449
+ show_metadata: true
450
+ )
451
+ end
452
+ ```
453
+
454
+ ---
455
+
456
+ ## 4. Web UI
457
+
458
+ ### 4.0. DevLog Adapter (File-Based JSONL Data Source)
459
+
460
+ **Web UI reads events from structured JSONL log file:**
461
+
462
+ ```ruby
463
+ # lib/e11y/adapters/dev_log.rb
464
+ module E11y
465
+ module Adapters
466
+ class DevLog < Base
467
+ attr_reader :file_path
468
+
469
+ def initialize(config = {})
470
+ super(name: :dev_log)
471
+ @file_path = config[:path] || Rails.root.join('log', 'e11y_dev.jsonl')
472
+ @max_lines = config[:max_lines] || 10_000
473
+ @max_size = config[:max_size] || 10.megabytes
474
+ @enable_watcher = config[:enable_watcher] != false
475
+ @mutex = Mutex.new
476
+
477
+ # Cache for Web UI (invalidated on file change)
478
+ @cache = nil
479
+ @cache_mtime = nil
480
+
481
+ setup_file_watcher if @enable_watcher
482
+ end
483
+
484
+ # ===================================================================
485
+ # ADAPTER INTERFACE (Write events)
486
+ # ===================================================================
487
+
488
+ def send_batch(events)
489
+ @mutex.synchronize do
490
+ FileUtils.mkdir_p(File.dirname(@file_path))
491
+
492
+ File.open(@file_path, 'a') do |f|
493
+ # Exclusive lock for multi-process safety
494
+ f.flock(File::LOCK_EX)
495
+
496
+ events.each do |event|
497
+ # JSONL format: one JSON object per line
498
+ f.puts({
499
+ id: SecureRandom.uuid,
500
+ timestamp: event[:timestamp],
501
+ event_name: event[:event_name],
502
+ severity: event[:severity],
503
+ payload: event[:payload],
504
+ trace_id: event[:trace_id],
505
+ span_id: event[:span_id],
506
+ metadata: event[:metadata]
507
+ }.to_json)
508
+ end
509
+
510
+ f.flock(File::LOCK_UN)
511
+ end
512
+
513
+ # Auto-rotate if needed
514
+ rotate_if_needed!
515
+
516
+ # Invalidate cache
517
+ @cache = nil
518
+ end
519
+
520
+ { success: true, sent: events.size }
521
+ end
522
+
523
+ # ===================================================================
524
+ # WEB UI API (Read events)
525
+ # ===================================================================
526
+
527
+ # Get all events (cached for performance)
528
+ def all_events(limit: 1000)
529
+ # Check if file changed
530
+ if cache_valid?
531
+ return @cache
532
+ end
533
+
534
+ # Read and parse file
535
+ @cache = read_events(limit)
536
+ @cache_mtime = file_mtime
537
+ @cache
538
+ rescue Errno::ENOENT
539
+ []
540
+ end
541
+
542
+ # Get event by ID (scan file)
543
+ def find_event(id)
544
+ return nil unless File.exist?(@file_path)
545
+
546
+ # Reverse scan for better performance (recent events first)
547
+ File.readlines(@file_path).reverse.each do |line|
548
+ event = JSON.parse(line, symbolize_names: true)
549
+ return event if event[:id] == id
550
+ end
551
+
552
+ nil
553
+ rescue JSON::ParserError
554
+ nil
555
+ end
556
+
557
+ # Filter by event name
558
+ def events_by_name(event_name, limit: 1000)
559
+ all_events(limit: limit * 2).select { |e| e[:event_name] == event_name }.first(limit)
560
+ end
561
+
562
+ # Filter by severity
563
+ def events_by_severity(severity, limit: 1000)
564
+ all_events(limit: limit * 2).select { |e| e[:severity] == severity }.first(limit)
565
+ end
566
+
567
+ # Filter by trace_id
568
+ def events_by_trace(trace_id)
569
+ all_events(limit: 10_000).select { |e| e[:trace_id] == trace_id }
570
+ end
571
+
572
+ # Search in payload
573
+ def search(query, limit: 1000)
574
+ query_downcase = query.downcase
575
+ all_events(limit: limit * 2).select do |event|
576
+ event.to_json.downcase.include?(query_downcase)
577
+ end.first(limit)
578
+ end
579
+
580
+ # Clear all events (for Web UI "Clear" button)
581
+ def clear!
582
+ @mutex.synchronize do
583
+ File.delete(@file_path) if File.exist?(@file_path)
584
+ @cache = nil
585
+ end
586
+ end
587
+
588
+ # Statistics
589
+ def stats
590
+ events = all_events(limit: 10_000)
591
+
592
+ {
593
+ total_events: events.size,
594
+ file_size: file_size_human,
595
+ by_severity: events.group_by { |e| e[:severity] }.transform_values(&:count),
596
+ by_event_name: events.group_by { |e| e[:event_name] }.transform_values(&:count),
597
+ oldest_event: events.first&.dig(:timestamp),
598
+ newest_event: events.last&.dig(:timestamp)
599
+ }
600
+ end
601
+
602
+ # Near-realtime: check if file updated since last read
603
+ def updated_since?(timestamp)
604
+ file_mtime > timestamp
605
+ rescue Errno::ENOENT
606
+ false
607
+ end
608
+
609
+ private
610
+
611
+ # ===================================================================
612
+ # CACHE & PERFORMANCE
613
+ # ===================================================================
614
+
615
+ def cache_valid?
616
+ @cache && @cache_mtime && file_mtime == @cache_mtime
617
+ end
618
+
619
+ def file_mtime
620
+ File.mtime(@file_path)
621
+ rescue Errno::ENOENT
622
+ Time.at(0)
623
+ end
624
+
625
+ def read_events(limit)
626
+ return [] unless File.exist?(@file_path)
627
+
628
+ # Read last N lines (most recent events)
629
+ lines = File.readlines(@file_path).last(limit)
630
+
631
+ # Parse JSON (with error handling for corrupted lines)
632
+ lines.map do |line|
633
+ JSON.parse(line, symbolize_names: true)
634
+ rescue JSON::ParserError => e
635
+ Rails.logger.warn("E11y: Failed to parse event line: #{e.message}")
636
+ nil
637
+ end.compact.reverse # Reverse for newest-first
638
+ end
639
+
640
+ # ===================================================================
641
+ # AUTO-ROTATION
642
+ # ===================================================================
643
+
644
+ def rotate_if_needed!
645
+ return unless File.exist?(@file_path)
646
+
647
+ # Check file size
648
+ if File.size(@file_path) > @max_size
649
+ rotate_file!
650
+ end
651
+ end
652
+
653
+ def rotate_file!
654
+ # Read last 50% of lines
655
+ lines = File.readlines(@file_path)
656
+ keep_lines = lines.last(@max_lines / 2)
657
+
658
+ # Backup old file
659
+ backup_path = "#{@file_path}.#{Time.now.to_i}.old"
660
+ FileUtils.mv(@file_path, backup_path)
661
+
662
+ # Write kept lines to new file
663
+ File.write(@file_path, keep_lines.join)
664
+
665
+ Rails.logger.info("E11y: Rotated log file (kept #{keep_lines.size} events)")
666
+ end
667
+
668
+ def file_size_human
669
+ size = File.size(@file_path)
670
+ units = ['B', 'KB', 'MB', 'GB']
671
+ unit = 0
672
+
673
+ while size > 1024 && unit < units.size - 1
674
+ size /= 1024.0
675
+ unit += 1
676
+ end
677
+
678
+ "#{size.round(2)} #{units[unit]}"
679
+ rescue Errno::ENOENT
680
+ '0 B'
681
+ end
682
+
683
+ # ===================================================================
684
+ # FILE WATCHER (Near-Realtime Updates)
685
+ # ===================================================================
686
+
687
+ def setup_file_watcher
688
+ # Use Listen gem for file watching (if available)
689
+ return unless defined?(Listen)
690
+
691
+ dir = File.dirname(@file_path)
692
+ filename = File.basename(@file_path)
693
+
694
+ @listener = Listen.to(dir, only: /#{Regexp.escape(filename)}/) do |modified, added, removed|
695
+ # Invalidate cache on file change
696
+ @cache = nil if modified.any? || added.any?
697
+ end
698
+
699
+ @listener.start
700
+ rescue => e
701
+ Rails.logger.warn("E11y: File watcher setup failed: #{e.message}")
702
+ end
703
+ end
704
+ end
705
+ end
706
+ ```
707
+
708
+ **Automatic Registration in Development:**
709
+
710
+ ```ruby
711
+ # lib/e11y/railtie.rb
712
+ module E11y
713
+ class Railtie < Rails::Railtie
714
+ initializer 'e11y.setup_development', after: :load_config_initializers do
715
+ if Rails.env.development? || Rails.env.test?
716
+ # Auto-register file-based adapter for Web UI
717
+ unless E11y.config.adapters[:dev_log]
718
+ E11y.config.adapters.register :dev_log, E11y::Adapters::DevLog.new(
719
+ path: Rails.root.join('log', 'e11y_dev.jsonl'),
720
+ max_lines: ENV['E11Y_MAX_EVENTS']&.to_i || 10_000,
721
+ max_size: (ENV['E11Y_MAX_SIZE']&.to_i || 10).megabytes,
722
+ enable_watcher: true
723
+ )
724
+ end
725
+
726
+ # Mount Web UI
727
+ Rails.application.routes.prepend do
728
+ mount E11y::WebUI::Engine, at: '/e11y', as: 'e11y_web_ui'
729
+ end
730
+ end
731
+ end
732
+ end
733
+ end
734
+ ```
735
+
736
+ **Access in Console/Web UI:**
737
+
738
+ ```ruby
739
+ # Rails console
740
+ >> E11y.dev_log_adapter
741
+ => #<E11y::Adapters::DevLog @file_path="log/e11y_dev.jsonl">
742
+
743
+ >> E11y.dev_log_adapter.all_events
744
+ => [{ event_name: 'Events::OrderCreated', payload: {...}, ... }, ...]
745
+
746
+ >> E11y.dev_log_adapter.clear!
747
+ => nil
748
+
749
+ # CLI: grep events
750
+ $ tail -f log/e11y_dev.jsonl | jq 'select(.severity == "error")'
751
+ $ grep "OrderCreated" log/e11y_dev.jsonl | jq .
752
+ ```
753
+
754
+ ---
755
+
756
+ ### 4.1. Web UI Engine
757
+
758
+ ```ruby
759
+ # lib/e11y/web_ui/engine.rb
760
+ module E11y
761
+ module WebUI
762
+ class Engine < Rails::Engine
763
+ isolate_namespace E11y::WebUI
764
+
765
+ config.generators.api_only = false
766
+
767
+ initializer 'e11y_web_ui.assets' do |app|
768
+ app.config.assets.precompile += %w[e11y_web_ui.css e11y_web_ui.js]
769
+ end
770
+
771
+ # Mount at /e11y in development
772
+ initializer 'e11y_web_ui.mount', before: :add_routing_paths do |app|
773
+ if Rails.env.development? || Rails.env.test?
774
+ app.routes.prepend do
775
+ mount E11y::WebUI::Engine, at: '/e11y', as: 'e11y_web_ui'
776
+ end
777
+ end
778
+ end
779
+ end
780
+ end
781
+ end
782
+ ```
783
+
784
+ ### 4.2. Web UI Controller
785
+
786
+ ```ruby
787
+ # app/controllers/e11y/web_ui/events_controller.rb
788
+ module E11y
789
+ module WebUI
790
+ class EventsController < ApplicationController
791
+ layout 'e11y/web_ui/application'
792
+
793
+ before_action :ensure_development_environment!
794
+ before_action :ensure_dev_log_adapter!
795
+
796
+ def index
797
+ # ✅ Read from file-based adapter
798
+ @events = dev_log_adapter.all_events(limit: 1000)
799
+ @event_names = @events.map { |e| e[:event_name] }.uniq.sort
800
+
801
+ # Filter by event name
802
+ if params[:event_name].present?
803
+ @events = dev_log_adapter.events_by_name(params[:event_name])
804
+ end
805
+
806
+ # Filter by severity
807
+ if params[:severity].present?
808
+ @events = dev_log_adapter.events_by_severity(params[:severity].to_sym)
809
+ end
810
+
811
+ # Search
812
+ if params[:q].present?
813
+ @events = dev_log_adapter.search(params[:q])
814
+ end
815
+
816
+ # Already sorted newest-first by adapter
817
+
818
+ # Paginate
819
+ @page = params[:page]&.to_i || 1
820
+ @per_page = 50
821
+ @total = @events.size
822
+ @events = @events.drop((@page - 1) * @per_page).first(@per_page)
823
+
824
+ # Stats for dashboard
825
+ @stats = dev_log_adapter.stats
826
+
827
+ # For near-realtime polling
828
+ @last_check = Time.now.to_f
829
+ end
830
+
831
+ def show
832
+ # ✅ Find by ID in file
833
+ @event = dev_log_adapter.find_event(params[:id])
834
+
835
+ if @event.nil?
836
+ redirect_to events_path, alert: 'Event not found'
837
+ end
838
+ end
839
+
840
+ def trace
841
+ # Show all events for specific trace_id
842
+ trace_id = params[:trace_id]
843
+ @events = dev_log_adapter.events_by_trace(trace_id)
844
+
845
+ render :index
846
+ end
847
+
848
+ def clear
849
+ # ✅ Clear file
850
+ dev_log_adapter.clear!
851
+ redirect_to events_path, notice: 'All events cleared'
852
+ end
853
+
854
+ def export
855
+ @events = dev_log_adapter.all_events(limit: 10_000)
856
+
857
+ respond_to do |format|
858
+ format.json { render json: @events }
859
+ format.csv do
860
+ send_data generate_csv(@events), filename: "events-#{Time.now.to_i}.csv"
861
+ end
862
+ end
863
+ end
864
+
865
+ # ===================================================================
866
+ # NEAR-REALTIME POLLING
867
+ # ===================================================================
868
+
869
+ def poll
870
+ # Check if file updated since last check
871
+ last_check_time = Time.at(params[:since].to_f)
872
+
873
+ if dev_log_adapter.updated_since?(last_check_time)
874
+ # File updated → return new events
875
+ @events = dev_log_adapter.all_events(limit: 100)
876
+
877
+ render json: {
878
+ updated: true,
879
+ events: @events,
880
+ timestamp: Time.now.to_f
881
+ }
882
+ else
883
+ # No updates
884
+ render json: {
885
+ updated: false,
886
+ timestamp: Time.now.to_f
887
+ }
888
+ end
889
+ end
890
+
891
+ private
892
+
893
+ def ensure_development_environment!
894
+ unless Rails.env.development? || Rails.env.test?
895
+ raise ActionController::RoutingError, 'E11y Web UI is only available in development/test'
896
+ end
897
+ end
898
+
899
+ def ensure_dev_log_adapter!
900
+ unless dev_log_adapter
901
+ raise 'E11y dev_log adapter not configured. Add to config/environments/development.rb'
902
+ end
903
+ end
904
+
905
+ def dev_log_adapter
906
+ @dev_log_adapter ||= E11y.config.adapters[:dev_log]
907
+ end
908
+
909
+ def generate_csv(events)
910
+ require 'csv'
911
+
912
+ CSV.generate do |csv|
913
+ csv << ['Timestamp', 'Event Name', 'Severity', 'Trace ID', 'Payload']
914
+
915
+ events.each do |event|
916
+ csv << [
917
+ event[:timestamp],
918
+ event[:event_name],
919
+ event[:severity],
920
+ event[:trace_id],
921
+ event[:payload].to_json
922
+ ]
923
+ end
924
+ end
925
+ end
926
+ end
927
+ end
928
+ end
929
+ ```
930
+
931
+ **Helper for accessing dev_log adapter:**
932
+
933
+ ```ruby
934
+ # lib/e11y.rb
935
+ module E11y
936
+ def self.dev_log_adapter
937
+ config.adapters[:dev_log]
938
+ end
939
+
940
+ # Alias for backward compatibility
941
+ def self.test_adapter
942
+ dev_log_adapter
943
+ end
944
+ end
945
+ ```
946
+
947
+ ### 4.3. Near-Realtime Polling (JavaScript)
948
+
949
+ ```javascript
950
+ // app/assets/javascripts/e11y/web_ui/realtime.js
951
+ // Near-realtime event polling (every 3 seconds)
952
+
953
+ class E11yRealtimePoller {
954
+ constructor() {
955
+ this.lastCheck = Date.now() / 1000; // Unix timestamp
956
+ this.polling = false;
957
+ this.interval = 3000; // Poll every 3 seconds
958
+ this.pollerId = null;
959
+ }
960
+
961
+ start() {
962
+ if (this.polling) return;
963
+
964
+ this.polling = true;
965
+ console.log('[E11y] Starting realtime polling...');
966
+
967
+ this.pollerId = setInterval(() => {
968
+ this.poll();
969
+ }, this.interval);
970
+ }
971
+
972
+ stop() {
973
+ if (!this.polling) return;
974
+
975
+ this.polling = false;
976
+ clearInterval(this.pollerId);
977
+ console.log('[E11y] Stopped realtime polling');
978
+ }
979
+
980
+ async poll() {
981
+ try {
982
+ const response = await fetch(`/e11y/poll?since=${this.lastCheck}`);
983
+ const data = await response.json();
984
+
985
+ if (data.updated) {
986
+ console.log('[E11y] New events detected, refreshing...');
987
+ this.onUpdate(data.events);
988
+ }
989
+
990
+ this.lastCheck = data.timestamp;
991
+ } catch (error) {
992
+ console.error('[E11y] Polling error:', error);
993
+ }
994
+ }
995
+
996
+ onUpdate(events) {
997
+ // Show notification badge
998
+ const badge = document.querySelector('.e11y-new-events-badge');
999
+ if (badge) {
1000
+ badge.textContent = `${events.length} new`;
1001
+ badge.style.display = 'inline-block';
1002
+ }
1003
+
1004
+ // Optional: Auto-refresh page
1005
+ const autoRefresh = document.querySelector('[data-e11y-auto-refresh]');
1006
+ if (autoRefresh && autoRefresh.checked) {
1007
+ window.location.reload();
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ // Auto-start on page load
1013
+ document.addEventListener('DOMContentLoaded', () => {
1014
+ const eventsPage = document.querySelector('[data-e11y-events-page]');
1015
+
1016
+ if (eventsPage) {
1017
+ window.e11yPoller = new E11yRealtimePoller();
1018
+ window.e11yPoller.start();
1019
+
1020
+ // Stop polling when page hidden (save resources)
1021
+ document.addEventListener('visibilitychange', () => {
1022
+ if (document.hidden) {
1023
+ window.e11yPoller.stop();
1024
+ } else {
1025
+ window.e11yPoller.start();
1026
+ }
1027
+ });
1028
+ }
1029
+ });
1030
+ ```
1031
+
1032
+ ### 4.4. Web UI Routes
1033
+
1034
+ ```ruby
1035
+ # config/routes.rb (E11y::WebUI::Engine)
1036
+ E11y::WebUI::Engine.routes.draw do
1037
+ root to: 'dashboard#index'
1038
+
1039
+ resources :events, only: [:index, :show] do
1040
+ collection do
1041
+ delete :clear
1042
+ get :export
1043
+ get :poll # ← Near-realtime polling endpoint
1044
+ end
1045
+ end
1046
+
1047
+ resources :registry, only: [:index, :show]
1048
+ resources :stats, only: [:index]
1049
+
1050
+ get '/timeline', to: 'timeline#index'
1051
+ get '/inspector/:id', to: 'inspector#show', as: :inspector
1052
+ end
1053
+ ```
1054
+
1055
+ ### 4.5. Web UI Views (Simplified)
1056
+
1057
+ ```erb
1058
+ <!-- app/views/e11y/web_ui/events/index.html.erb -->
1059
+ <div class="e11y-container" data-e11y-events-page>
1060
+ <header>
1061
+ <h1>
1062
+ E11y Event Explorer
1063
+ <span class="e11y-new-events-badge" style="display: none;">
1064
+ 0 new
1065
+ </span>
1066
+ </h1>
1067
+
1068
+ <div class="actions">
1069
+ <label class="e11y-auto-refresh">
1070
+ <input type="checkbox" data-e11y-auto-refresh />
1071
+ Auto-refresh on new events
1072
+ </label>
1073
+
1074
+ <%= link_to 'Refresh Now', events_path, class: 'btn btn-primary' %>
1075
+ <%= link_to 'Clear All', clear_events_path, method: :delete,
1076
+ data: { confirm: 'Clear all events?' }, class: 'btn btn-danger' %>
1077
+ <%= link_to 'Export JSON', export_events_path(format: :json), class: 'btn btn-secondary' %>
1078
+ <%= link_to 'Export CSV', export_events_path(format: :csv), class: 'btn btn-secondary' %>
1079
+ </div>
1080
+ </header>
1081
+
1082
+ <div class="filters">
1083
+ <%= form_with url: events_path, method: :get, local: true do |f| %>
1084
+ <%= f.select :event_name, @event_names, { include_blank: 'All Events' },
1085
+ class: 'form-select', onchange: 'this.form.submit()' %>
1086
+ <% end %>
1087
+ </div>
1088
+
1089
+ <div class="events-list">
1090
+ <% @events.each do |event| %>
1091
+ <div class="event-card <%= event[:severity] %>">
1092
+ <div class="event-header">
1093
+ <span class="event-time"><%= event[:timestamp] %></span>
1094
+ <span class="event-severity <%= event[:severity] %>">
1095
+ <%= event[:severity].to_s.upcase %>
1096
+ </span>
1097
+ <span class="event-name"><%= event[:event_name] %></span>
1098
+ </div>
1099
+
1100
+ <div class="event-payload">
1101
+ <pre><%= JSON.pretty_generate(event[:payload]) %></pre>
1102
+ </div>
1103
+
1104
+ <div class="event-meta">
1105
+ <span>Trace: <%= event[:trace_id] %></span>
1106
+ <%= link_to 'Inspect', inspector_path(event[:id]), class: 'btn-link' %>
1107
+ </div>
1108
+ </div>
1109
+ <% end %>
1110
+ </div>
1111
+
1112
+ <div class="pagination">
1113
+ <%= link_to_previous_page @events, 'Previous' %>
1114
+ Page <%= @page %>
1115
+ <%= link_to_next_page @events, 'Next' %>
1116
+ </div>
1117
+
1118
+ <!-- CSS for near-realtime badge -->
1119
+ <style>
1120
+ .e11y-new-events-badge {
1121
+ background: #28a745;
1122
+ color: white;
1123
+ padding: 4px 12px;
1124
+ border-radius: 16px;
1125
+ font-size: 0.75em;
1126
+ font-weight: 600;
1127
+ margin-left: 12px;
1128
+ animation: pulse 1.5s ease-in-out infinite;
1129
+ box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
1130
+ }
1131
+
1132
+ @keyframes pulse {
1133
+ 0%, 100% {
1134
+ opacity: 1;
1135
+ transform: scale(1);
1136
+ }
1137
+ 50% {
1138
+ opacity: 0.6;
1139
+ transform: scale(1.05);
1140
+ }
1141
+ }
1142
+
1143
+ .e11y-auto-refresh {
1144
+ display: inline-flex;
1145
+ align-items: center;
1146
+ gap: 8px;
1147
+ padding: 8px 16px;
1148
+ background: #f8f9fa;
1149
+ border-radius: 6px;
1150
+ cursor: pointer;
1151
+ transition: background 0.2s;
1152
+ }
1153
+
1154
+ .e11y-auto-refresh:hover {
1155
+ background: #e9ecef;
1156
+ }
1157
+
1158
+ .e11y-auto-refresh input[type="checkbox"] {
1159
+ cursor: pointer;
1160
+ }
1161
+ </style>
1162
+ </div>
1163
+ ```
1164
+
1165
+ **CLI Usage:**
1166
+
1167
+ ```bash
1168
+ # Watch events in realtime
1169
+ $ tail -f log/e11y_dev.jsonl | jq
1170
+
1171
+ # Filter by severity
1172
+ $ grep '"severity":"error"' log/e11y_dev.jsonl | jq
1173
+
1174
+ # Filter by event name
1175
+ $ grep 'OrderCreated' log/e11y_dev.jsonl | jq
1176
+
1177
+ # Count events by severity
1178
+ $ cat log/e11y_dev.jsonl | jq -r '.severity' | sort | uniq -c
1179
+
1180
+ # Last 10 events
1181
+ $ tail -10 log/e11y_dev.jsonl | jq
1182
+ ```
1183
+
1184
+ ---
1185
+
1186
+ ## 5. Event Registry
1187
+
1188
+ ### 5.1. Registry API (Extended from ADR-012)
1189
+
1190
+ ```ruby
1191
+ # lib/e11y/registry.rb (extended)
1192
+ module E11y
1193
+ class Registry
1194
+ # ... (previous methods from ADR-012) ...
1195
+
1196
+ # Introspection methods for developers
1197
+ def self.introspect(event_class_or_name)
1198
+ event_class = resolve_class(event_class_or_name)
1199
+ return nil unless event_class
1200
+
1201
+ {
1202
+ name: event_class.name,
1203
+ event_name: event_class.event_name,
1204
+ version: event_class.event_version,
1205
+ schema: extract_schema(event_class),
1206
+ adapters: event_class.adapters,
1207
+ severity: event_class.severity,
1208
+ deprecated: event_class.deprecated?,
1209
+ migration_rules: event_class.migration_rules.keys
1210
+ }
1211
+ end
1212
+
1213
+ def self.search(query)
1214
+ @events.keys.select { |name| name.downcase.include?(query.downcase) }
1215
+ end
1216
+
1217
+ def self.by_adapter(adapter_name)
1218
+ all_event_classes.select do |event_class|
1219
+ event_class.adapters.include?(adapter_name)
1220
+ end
1221
+ end
1222
+
1223
+ def self.by_severity(severity)
1224
+ all_event_classes.select do |event_class|
1225
+ event_class.severity == severity
1226
+ end
1227
+ end
1228
+
1229
+ def self.statistics
1230
+ {
1231
+ total_events: @events.size,
1232
+ total_versions: @events.sum { |_name, versions| versions.size },
1233
+ deprecated_count: deprecated_events.size,
1234
+ by_severity: count_by_severity,
1235
+ by_adapter: count_by_adapter
1236
+ }
1237
+ end
1238
+
1239
+ private
1240
+
1241
+ def self.resolve_class(event_class_or_name)
1242
+ case event_class_or_name
1243
+ when Class
1244
+ event_class_or_name
1245
+ when String
1246
+ latest_version(event_class_or_name)
1247
+ else
1248
+ nil
1249
+ end
1250
+ end
1251
+
1252
+ def self.extract_schema(event_class)
1253
+ return {} unless event_class.respond_to?(:schema)
1254
+
1255
+ schema = event_class.schema
1256
+
1257
+ # Extract field definitions
1258
+ schema.rules.map do |key, rule|
1259
+ {
1260
+ name: key,
1261
+ type: rule.type,
1262
+ required: rule.required?,
1263
+ options: rule.options
1264
+ }
1265
+ end
1266
+ end
1267
+
1268
+ def self.all_event_classes
1269
+ @events.values.flat_map(&:values)
1270
+ end
1271
+
1272
+ def self.count_by_severity
1273
+ all_event_classes.group_by(&:severity).transform_values(&:count)
1274
+ end
1275
+
1276
+ def self.count_by_adapter
1277
+ adapter_counts = Hash.new(0)
1278
+
1279
+ all_event_classes.each do |event_class|
1280
+ event_class.adapters.each do |adapter|
1281
+ adapter_counts[adapter] += 1
1282
+ end
1283
+ end
1284
+
1285
+ adapter_counts
1286
+ end
1287
+ end
1288
+ end
1289
+ ```
1290
+
1291
+ ### 5.2. Registry Console API
1292
+
1293
+ ```ruby
1294
+ # Rails console helpers
1295
+ module E11y
1296
+ module Console
1297
+ def self.setup!
1298
+ define_helper_methods
1299
+ end
1300
+
1301
+ def self.define_helper_methods
1302
+ # List all events
1303
+ def E11y.events
1304
+ Registry.all_events
1305
+ end
1306
+
1307
+ # Search events
1308
+ def E11y.search(query)
1309
+ Registry.search(query)
1310
+ end
1311
+
1312
+ # Introspect event
1313
+ def E11y.inspect(event_class_or_name)
1314
+ info = Registry.introspect(event_class_or_name)
1315
+
1316
+ puts "Event: #{info[:name]}"
1317
+ puts " Version: #{info[:version]}"
1318
+ puts " Event Name: #{info[:event_name]}"
1319
+ puts " Severity: #{info[:severity]}"
1320
+ puts " Adapters: #{info[:adapters].join(', ')}"
1321
+ puts " Deprecated: #{info[:deprecated] ? 'Yes' : 'No'}"
1322
+ puts ""
1323
+ puts "Schema:"
1324
+ info[:schema].each do |field|
1325
+ required = field[:required] ? 'required' : 'optional'
1326
+ puts " #{field[:name]} (#{field[:type]}, #{required})"
1327
+ end
1328
+
1329
+ nil
1330
+ end
1331
+
1332
+ # Registry statistics
1333
+ def E11y.stats
1334
+ Registry.statistics
1335
+ end
1336
+
1337
+ # Test event tracking
1338
+ def E11y.test(event_class = nil)
1339
+ event_class ||= Events::TestEvent
1340
+
1341
+ event_class.track(
1342
+ test: true,
1343
+ timestamp: Time.now,
1344
+ message: 'Test event from console'
1345
+ )
1346
+
1347
+ puts "✅ Test event tracked: #{event_class.name}"
1348
+ end
1349
+ end
1350
+ end
1351
+ end
1352
+ ```
1353
+
1354
+ ### 5.3. Usage Examples
1355
+
1356
+ ```ruby
1357
+ # Rails console
1358
+ >> E11y.events
1359
+ => ["Events::OrderCreated", "Events::OrderPaid", "Events::PaymentProcessed"]
1360
+
1361
+ >> E11y.search('order')
1362
+ => ["Events::OrderCreated", "Events::OrderPaid", "Events::OrderCancelled"]
1363
+
1364
+ >> E11y.inspect(Events::OrderCreated)
1365
+ Event: Events::OrderCreated
1366
+ Version: 1
1367
+ Event Name: Events::OrderCreated
1368
+ Severity: info
1369
+ Adapters: loki, sentry
1370
+ Deprecated: No
1371
+
1372
+ Schema:
1373
+ order_id (string, required)
1374
+ user_id (integer, required)
1375
+ amount (float, required)
1376
+ currency (string, optional)
1377
+
1378
+ >> E11y.stats
1379
+ => {
1380
+ total_events: 25,
1381
+ total_versions: 28,
1382
+ deprecated_count: 3,
1383
+ by_severity: { debug: 5, info: 15, warn: 3, error: 2 },
1384
+ by_adapter: { loki: 20, sentry: 15, file: 25 }
1385
+ }
1386
+
1387
+ >> E11y.test
1388
+ ✅ Test event tracked: Events::TestEvent
1389
+ ```
1390
+
1391
+ ---
1392
+
1393
+ ## 6. Debug Helpers
1394
+
1395
+ ### 6.1. Pipeline Inspector
1396
+
1397
+ ```ruby
1398
+ # lib/e11y/debug/pipeline_inspector.rb
1399
+ module E11y
1400
+ module Debug
1401
+ class PipelineInspector
1402
+ def self.trace_event(event_class, payload)
1403
+ puts colorize("🔍 Tracing Event Pipeline", :cyan, :bold)
1404
+ puts ""
1405
+
1406
+ # Step 1: Validation
1407
+ trace_step("Validation") do
1408
+ event_class.validate_payload(payload)
1409
+ end
1410
+
1411
+ # Step 2: PII Filtering
1412
+ trace_step("PII Filtering") do
1413
+ filtered = E11y::Security::PIIFilter.filter(payload)
1414
+ puts " Filtered fields: #{filtered.keys.join(', ')}" if filtered.any?
1415
+ end
1416
+
1417
+ # Step 3: Sampling
1418
+ trace_step("Sampling") do
1419
+ sampled = E11y::Sampler.should_sample?(event_class, payload)
1420
+ puts " Decision: #{sampled ? 'SAMPLED' : 'DROPPED'}"
1421
+ end
1422
+
1423
+ # Step 4: Rate Limiting
1424
+ trace_step("Rate Limiting") do
1425
+ allowed = E11y::RateLimiter.allow?(event_class, payload)
1426
+ puts " Decision: #{allowed ? 'ALLOWED' : 'RATE LIMITED'}"
1427
+ end
1428
+
1429
+ # Step 5: Adapter Routing
1430
+ trace_step("Adapter Routing") do
1431
+ adapters = E11y::Router.route(event_class, payload)
1432
+ puts " Target adapters: #{adapters.join(', ')}"
1433
+ end
1434
+
1435
+ # Step 6: Buffering
1436
+ trace_step("Buffering") do
1437
+ buffer_size = E11y::Buffer.size
1438
+ puts " Current buffer: #{buffer_size} events"
1439
+ end
1440
+
1441
+ puts ""
1442
+ puts colorize("✅ Pipeline trace complete", :green, :bold)
1443
+ end
1444
+
1445
+ private
1446
+
1447
+ def self.trace_step(name, &block)
1448
+ print colorize(" #{name}... ", :yellow)
1449
+
1450
+ begin
1451
+ block.call
1452
+ puts colorize("✓", :green)
1453
+ rescue => error
1454
+ puts colorize("✗ #{error.message}", :red)
1455
+ end
1456
+ end
1457
+
1458
+ def self.colorize(text, color, style = nil)
1459
+ # Same as Console adapter
1460
+ end
1461
+ end
1462
+ end
1463
+ end
1464
+ ```
1465
+
1466
+ ### 6.2. Usage
1467
+
1468
+ ```ruby
1469
+ # Rails console
1470
+ >> E11y::Debug::PipelineInspector.trace_event(Events::OrderCreated, {
1471
+ order_id: 123,
1472
+ user_id: 456,
1473
+ amount: 99.99,
1474
+ credit_card: '4111-1111-1111-1111' # PII
1475
+ })
1476
+
1477
+ 🔍 Tracing Event Pipeline
1478
+
1479
+ Validation... ✓
1480
+ PII Filtering... ✓
1481
+ Filtered fields: credit_card
1482
+ Sampling... ✓
1483
+ Decision: SAMPLED
1484
+ Rate Limiting... ✓
1485
+ Decision: ALLOWED
1486
+ Adapter Routing... ✓
1487
+ Target adapters: loki, sentry
1488
+ Buffering... ✓
1489
+ Current buffer: 5 events
1490
+
1491
+ ✅ Pipeline trace complete
1492
+ ```
1493
+
1494
+ ---
1495
+
1496
+ ## 7. CLI Tools
1497
+
1498
+ ### 7.1. Rake Tasks
1499
+
1500
+ ```ruby
1501
+ # lib/tasks/e11y.rake
1502
+ namespace :e11y do
1503
+ desc 'List all registered events'
1504
+ task list: :environment do
1505
+ puts "E11y Event Registry"
1506
+ puts "=" * 80
1507
+ puts ""
1508
+
1509
+ E11y::Registry.all_events.each do |event_name|
1510
+ versions = E11y::Registry.all_versions(event_name)
1511
+
1512
+ versions.each do |event_class|
1513
+ status = event_class.deprecated? ? "[DEPRECATED]" : ""
1514
+ puts "#{event_class.name} (v#{event_class.event_version}) #{status}"
1515
+ puts " Severity: #{event_class.severity}"
1516
+ puts " Adapters: #{event_class.adapters.join(', ')}"
1517
+ puts ""
1518
+ end
1519
+ end
1520
+
1521
+ stats = E11y::Registry.statistics
1522
+ puts "=" * 80
1523
+ puts "Total: #{stats[:total_events]} events, #{stats[:total_versions]} versions"
1524
+ puts "Deprecated: #{stats[:deprecated_count]}"
1525
+ end
1526
+
1527
+ desc 'Validate all event schemas'
1528
+ task validate: :environment do
1529
+ puts "Validating E11y Event Schemas"
1530
+ puts "=" * 80
1531
+
1532
+ errors = []
1533
+
1534
+ E11y::Registry.all_versions('*').each do |event_class|
1535
+ begin
1536
+ # Check if schema is defined
1537
+ unless event_class.respond_to?(:schema)
1538
+ errors << "#{event_class.name}: Missing schema definition"
1539
+ next
1540
+ end
1541
+
1542
+ # Validate schema syntax
1543
+ schema = event_class.schema
1544
+
1545
+ puts "✓ #{event_class.name}"
1546
+ rescue => error
1547
+ errors << "#{event_class.name}: #{error.message}"
1548
+ puts "✗ #{event_class.name}: #{error.message}"
1549
+ end
1550
+ end
1551
+
1552
+ puts ""
1553
+ puts "=" * 80
1554
+
1555
+ if errors.empty?
1556
+ puts "✅ All schemas valid"
1557
+ else
1558
+ puts "❌ #{errors.size} errors found:"
1559
+ errors.each { |e| puts " - #{e}" }
1560
+ exit 1
1561
+ end
1562
+ end
1563
+
1564
+ desc 'Generate event documentation'
1565
+ task 'docs:generate' => :environment do
1566
+ require 'e11y/documentation/generator'
1567
+
1568
+ puts "Generating E11y Documentation"
1569
+ puts "=" * 80
1570
+
1571
+ output_dir = Rails.root.join('docs', 'events')
1572
+ FileUtils.mkdir_p(output_dir)
1573
+
1574
+ E11y::Documentation::Generator.generate(output_dir)
1575
+
1576
+ puts ""
1577
+ puts "✅ Documentation generated in #{output_dir}"
1578
+ end
1579
+
1580
+ desc 'Show E11y statistics'
1581
+ task stats: :environment do
1582
+ stats = E11y::Registry.statistics
1583
+
1584
+ puts "E11y Statistics"
1585
+ puts "=" * 80
1586
+ puts ""
1587
+ puts "Events: #{stats[:total_events]}"
1588
+ puts "Versions: #{stats[:total_versions]}"
1589
+ puts "Deprecated: #{stats[:deprecated_count]}"
1590
+ puts ""
1591
+ puts "By Severity:"
1592
+ stats[:by_severity].each do |severity, count|
1593
+ puts " #{severity}: #{count}"
1594
+ end
1595
+ puts ""
1596
+ puts "By Adapter:"
1597
+ stats[:by_adapter].each do |adapter, count|
1598
+ puts " #{adapter}: #{count}"
1599
+ end
1600
+ end
1601
+ end
1602
+ ```
1603
+
1604
+ ---
1605
+
1606
+ ## 8. Documentation Generation
1607
+
1608
+ ### 8.1. Documentation Generator
1609
+
1610
+ ```ruby
1611
+ # lib/e11y/documentation/generator.rb
1612
+ module E11y
1613
+ module Documentation
1614
+ class Generator
1615
+ def self.generate(output_dir)
1616
+ # Generate index
1617
+ generate_index(output_dir)
1618
+
1619
+ # Generate per-event documentation
1620
+ E11y::Registry.all_events.each do |event_name|
1621
+ generate_event_docs(event_name, output_dir)
1622
+ end
1623
+
1624
+ # Generate catalog
1625
+ generate_catalog(output_dir)
1626
+ end
1627
+
1628
+ private
1629
+
1630
+ def self.generate_index(output_dir)
1631
+ content = <<~MARKDOWN
1632
+ # E11y Event Documentation
1633
+
1634
+ Auto-generated documentation for all events in this application.
1635
+
1636
+ **Last updated:** #{Time.now}
1637
+
1638
+ ## Statistics
1639
+
1640
+ - Total Events: #{E11y::Registry.statistics[:total_events]}
1641
+ - Total Versions: #{E11y::Registry.statistics[:total_versions]}
1642
+ - Deprecated: #{E11y::Registry.statistics[:deprecated_count]}
1643
+
1644
+ ## Events
1645
+
1646
+ #{generate_event_list}
1647
+ MARKDOWN
1648
+
1649
+ File.write(output_dir.join('README.md'), content)
1650
+ end
1651
+
1652
+ def self.generate_event_list
1653
+ E11y::Registry.all_events.sort.map do |event_name|
1654
+ latest = E11y::Registry.latest_version(event_name)
1655
+ "- [#{latest.name}](#{event_name.gsub('::', '_')}.md)"
1656
+ end.join("\n")
1657
+ end
1658
+
1659
+ def self.generate_event_docs(event_name, output_dir)
1660
+ versions = E11y::Registry.all_versions(event_name)
1661
+ latest = versions.last
1662
+
1663
+ content = <<~MARKDOWN
1664
+ # #{latest.name}
1665
+
1666
+ **Current Version:** #{latest.event_version}
1667
+ **Severity:** #{latest.severity}
1668
+ **Adapters:** #{latest.adapters.join(', ')}
1669
+ **Deprecated:** #{latest.deprecated? ? 'Yes' : 'No'}
1670
+
1671
+ ## Schema
1672
+
1673
+ ```ruby
1674
+ #{generate_schema_example(latest)}
1675
+ ```
1676
+
1677
+ ## Usage
1678
+
1679
+ ```ruby
1680
+ #{latest.name}.track(
1681
+ #{generate_payload_example(latest)}
1682
+ )
1683
+ ```
1684
+
1685
+ ## Versions
1686
+
1687
+ #{generate_version_history(versions)}
1688
+
1689
+ ## Examples
1690
+
1691
+ #{generate_examples(latest)}
1692
+ MARKDOWN
1693
+
1694
+ filename = event_name.gsub('::', '_') + '.md'
1695
+ File.write(output_dir.join(filename), content)
1696
+ end
1697
+
1698
+ def self.generate_schema_example(event_class)
1699
+ info = E11y::Registry.introspect(event_class)
1700
+
1701
+ info[:schema].map do |field|
1702
+ required = field[:required] ? 'required' : 'optional'
1703
+ "#{required}(:#{field[:name]}).filled(:#{field[:type]})"
1704
+ end.join("\n")
1705
+ end
1706
+
1707
+ def self.generate_payload_example(event_class)
1708
+ info = E11y::Registry.introspect(event_class)
1709
+
1710
+ info[:schema].map do |field|
1711
+ example_value = case field[:type]
1712
+ when :string then "'example'"
1713
+ when :integer then '123'
1714
+ when :float then '99.99'
1715
+ when :bool then 'true'
1716
+ else 'value'
1717
+ end
1718
+
1719
+ "#{field[:name]}: #{example_value}"
1720
+ end.join(",\n ")
1721
+ end
1722
+
1723
+ def self.generate_version_history(versions)
1724
+ versions.reverse.map do |version|
1725
+ deprecated = version.deprecated? ? ' **(DEPRECATED)**' : ''
1726
+ "- **v#{version.event_version}**#{deprecated}"
1727
+ end.join("\n")
1728
+ end
1729
+
1730
+ def self.generate_examples(event_class)
1731
+ <<~MARKDOWN
1732
+ ### Basic Example
1733
+
1734
+ ```ruby
1735
+ #{event_class.name}.track(
1736
+ #{generate_payload_example(event_class)}
1737
+ )
1738
+ ```
1739
+
1740
+ ### With Duration
1741
+
1742
+ ```ruby
1743
+ #{event_class.name}.track_with_duration do
1744
+ # Your code here
1745
+ end
1746
+ ```
1747
+ MARKDOWN
1748
+ end
1749
+
1750
+ def self.generate_catalog(output_dir)
1751
+ content = <<~MARKDOWN
1752
+ # Event Catalog
1753
+
1754
+ Complete catalog of all events grouped by severity and adapter.
1755
+
1756
+ ## By Severity
1757
+
1758
+ #{generate_by_severity}
1759
+
1760
+ ## By Adapter
1761
+
1762
+ #{generate_by_adapter}
1763
+ MARKDOWN
1764
+
1765
+ File.write(output_dir.join('CATALOG.md'), content)
1766
+ end
1767
+
1768
+ def self.generate_by_severity
1769
+ stats = E11y::Registry.statistics
1770
+
1771
+ stats[:by_severity].map do |severity, count|
1772
+ events = E11y::Registry.by_severity(severity)
1773
+
1774
+ <<~MARKDOWN
1775
+ ### #{severity.to_s.capitalize} (#{count})
1776
+
1777
+ #{events.map { |e| "- #{e.name}" }.join("\n")}
1778
+ MARKDOWN
1779
+ end.join("\n")
1780
+ end
1781
+
1782
+ def self.generate_by_adapter
1783
+ stats = E11y::Registry.statistics
1784
+
1785
+ stats[:by_adapter].map do |adapter, count|
1786
+ events = E11y::Registry.by_adapter(adapter)
1787
+
1788
+ <<~MARKDOWN
1789
+ ### #{adapter} (#{count})
1790
+
1791
+ #{events.map { |e| "- #{e.name}" }.join("\n")}
1792
+ MARKDOWN
1793
+ end.join("\n")
1794
+ end
1795
+ end
1796
+ end
1797
+ end
1798
+ ```
1799
+
1800
+ ---
1801
+
1802
+ ## 9. Trade-offs
1803
+
1804
+ ### 9.1. Key Decisions
1805
+
1806
+ | Decision | Pro | Con | Rationale |
1807
+ |----------|-----|-----|-----------|
1808
+ | **Console adapter** | Beautiful DX | Performance overhead | Dev only |
1809
+ | **Web UI** | Visual exploration | Maintenance burden | Critical for DX |
1810
+ | **File-based JSONL** | Multi-process, persistent, zero deps | ~50ms read latency | Multi-process support > speed |
1811
+ | **Near-realtime (3s)** | Simple, no WebSocket | 3s delay | Good enough for dev |
1812
+ | **Event Registry** | Full introspection | Memory overhead | Worth it |
1813
+ | **Auto-generated docs** | Always up-to-date | Limited customization | Consistency |
1814
+ | **Dev-only features** | Safe | Not in production | Clear separation |
1815
+ | **Max 10K events** | Auto-rotation | Limited history | Enough for debugging |
1816
+ | **Auto-rotate at 10MB** | Controlled size | File ops overhead | Prevents infinite growth |
1817
+
1818
+ ### 9.2. Alternatives Considered
1819
+
1820
+ **A) In-Memory adapter**
1821
+ ```ruby
1822
+ # ❌ REJECTED: Broken for multi-process!
1823
+ Puma 4 workers → 4 separate memory stores
1824
+ Web UI request → random worker → sees only 25% of events
1825
+ ```
1826
+ - ❌ Rejected: Doesn't work with Puma/Unicorn
1827
+ - ✅ Chosen: File-based (multi-process safe)
1828
+
1829
+ **B) Query Loki/Elasticsearch for Web UI**
1830
+ ```ruby
1831
+ # ❌ REJECTED
1832
+ def index
1833
+ @events = Loki.query('...') # Requires Loki in dev!
1834
+ end
1835
+ ```
1836
+ - ❌ Requires infrastructure in dev (Loki, ES)
1837
+ - ❌ Slow queries (HTTP, parsing)
1838
+ - ❌ Complex setup for developers
1839
+ - ❌ Can't work offline
1840
+
1841
+ **C) Store events in Redis**
1842
+ ```ruby
1843
+ # ❌ REJECTED
1844
+ config.adapters.register :redis, E11y::Adapters::Redis.new
1845
+ ```
1846
+ - ✅ Persistent across restarts
1847
+ - ✅ Multi-process safe
1848
+ - ❌ Requires Redis in dev
1849
+ - ❌ Extra dependency
1850
+
1851
+ **D) Store events in Database**
1852
+ ```ruby
1853
+ # ❌ REJECTED
1854
+ create_table :e11y_events do |t|
1855
+ t.string :event_name
1856
+ t.json :payload
1857
+ ...
1858
+ end
1859
+ ```
1860
+ - ✅ Persistent
1861
+ - ✅ Multi-process safe
1862
+ - ❌ Pollutes application database
1863
+ - ❌ Migration needed
1864
+ - ❌ Not isolated from app data
1865
+
1866
+ **E) File-Based JSONL (CHOSEN) ✅**
1867
+ ```ruby
1868
+ # ✅ CHOSEN
1869
+ config.adapters.register :dev_log, E11y::Adapters::DevLog.new(
1870
+ path: Rails.root.join('log', 'e11y_dev.jsonl'),
1871
+ max_lines: 10_000,
1872
+ max_size: 10.megabytes
1873
+ )
1874
+ ```
1875
+ - ✅ Zero dependencies
1876
+ - ✅ Multi-process safe (append-only file)
1877
+ - ✅ Persistent across restarts
1878
+ - ✅ Isolated (no DB pollution)
1879
+ - ✅ Greppable: `tail -f | jq`
1880
+ - ✅ Auto-rotation
1881
+ - ✅ Thread-safe (mutex + file locking)
1882
+ - ⚠️ ~50ms read latency (acceptable for dev)
1883
+
1884
+ **F) WebSocket for realtime**
1885
+ ```ruby
1886
+ # ❌ REJECTED: Too complex for dev
1887
+ # Requires Action Cable, Redis, WebSocket setup
1888
+ ```
1889
+ - ❌ Rejected: Overkill for development
1890
+ - ✅ **CHOSEN**: Polling every 3 seconds (simple, good enough)
1891
+
1892
+ **G) No Web UI**
1893
+ - ❌ Rejected: Too important for DX
1894
+
1895
+ **H) Separate gem for Web UI**
1896
+ - ❌ Rejected: Adds complexity
1897
+
1898
+ **G) Manual documentation**
1899
+ - ❌ Rejected: Gets out of sync
1900
+
1901
+ **H) No console output**
1902
+ - ❌ Rejected: Critical for debugging
1903
+
1904
+ ---
1905
+
1906
+ ### 9.3. Why In-Memory is Right Choice for Development
1907
+
1908
+ **Development Workflow:**
1909
+ ```
1910
+ 1. Developer changes code
1911
+ 2. Server restarts (automatically)
1912
+ 3. Developer tests feature
1913
+ 4. Views events in Web UI ← Fresh data from current session
1914
+ 5. Repeats
1915
+ ```
1916
+
1917
+ **Key Insight:** File-based storage provides persistence across restarts!
1918
+ - Events survive server restarts (helpful for debugging)
1919
+ - Multi-process safe (works with Puma/Unicorn)
1920
+ - Auto-rotation keeps file size manageable
1921
+ - 10K events + 10MB limit → sufficient for dev sessions
1922
+
1923
+ **Disk Usage:**
1924
+ ```bash
1925
+ # Typical:
1926
+ 10,000 events × ~500 bytes/event = ~5MB disk
1927
+ ```
1928
+ → Negligible overhead
1929
+
1930
+ **Comparison:**
1931
+
1932
+ | Metric | File-Based JSONL | Redis | Database | Loki Query |
1933
+ |--------|------------------|-------|----------|------------|
1934
+ | **Setup** | 0 min | 5 min | 2 min | 30 min |
1935
+ | **Dependencies** | None | Redis | Migration | Loki + Docker |
1936
+ | **Write** | ~0.1ms | ~1ms | ~20ms | ~500ms |
1937
+ | **Read** | ~50ms | ~5ms | ~30ms | ~500ms |
1938
+ | **Persistence** | ✅ | ✅ | ✅ | ✅ |
1939
+ | **Multi-process** | ✅ | ✅ | ✅ | ✅ |
1940
+ | **Offline** | ✅ | ❌ | ✅ | ❌ |
1941
+ | **Disk/Memory** | ~5MB | ~10MB | ~10MB | 0MB |
1942
+ | **Cleanup** | Auto | Manual | Manual | N/A |
1943
+ | **CLI friendly** | ✅ `tail -f` | ❌ | ❌ | ❌ |
1944
+
1945
+ **Winner:** File-Based JSONL (best DX, zero friction, multi-process safe)
1946
+
1947
+ ---
1948
+
1949
+ ### 9.4. Production Safety
1950
+
1951
+ **How we prevent dev_log adapter in production:**
1952
+
1953
+ ```ruby
1954
+ # lib/e11y/railtie.rb
1955
+ initializer 'e11y.setup_development' do
1956
+ # ✅ Only auto-register in dev/test
1957
+ if Rails.env.development? || Rails.env.test?
1958
+ E11y.config.adapters.register :dev_log, E11y::Adapters::DevLog.new(
1959
+ path: Rails.root.join('log', 'e11y_dev.jsonl')
1960
+ )
1961
+ end
1962
+ end
1963
+
1964
+ # lib/e11y/web_ui/engine.rb
1965
+ initializer 'e11y_web_ui.mount' do |app|
1966
+ # ✅ Only mount Web UI in dev/test
1967
+ if Rails.env.development? || Rails.env.test?
1968
+ app.routes.prepend do
1969
+ mount E11y::WebUI::Engine, at: '/e11y'
1970
+ end
1971
+ end
1972
+ end
1973
+ ```
1974
+
1975
+ **What happens if someone tries to mount in production?**
1976
+
1977
+ ```ruby
1978
+ # config/routes.rb (production)
1979
+ mount E11y::WebUI::Engine, at: '/e11y' # ❌ Will raise error!
1980
+
1981
+ # app/controllers/e11y/web_ui/events_controller.rb
1982
+ before_action :ensure_development_environment!
1983
+
1984
+ def ensure_development_environment!
1985
+ unless Rails.env.development? || Rails.env.test?
1986
+ raise ActionController::RoutingError,
1987
+ 'E11y Web UI is only available in development/test'
1988
+ end
1989
+ end
1990
+ ```
1991
+
1992
+ → **Fail-safe:** Web UI physically cannot work in production
1993
+
1994
+ ---
1995
+
1996
+ ## 10. FAQ: File-Based Adapter & Web UI
1997
+
1998
+ ### Q1: Why do events persist across server restarts?
1999
+
2000
+ **A:** This is a **feature!** File-based storage provides persistence:
2001
+ - Events survive server restarts (helpful for debugging)
2002
+ - Can review events from previous sessions
2003
+ - No data loss on server restart
2004
+
2005
+ If you want to clear old events:
2006
+ ```bash
2007
+ rm log/e11y_dev.jsonl
2008
+ # or via Web UI: "Clear All" button
2009
+ ```
2010
+
2011
+ ### Q2: What if I need >10,000 events?
2012
+
2013
+ **A:** Increase limits:
2014
+ ```ruby
2015
+ config.adapters.register :dev_log, E11y::Adapters::DevLog.new(
2016
+ path: Rails.root.join('log', 'e11y_dev.jsonl'),
2017
+ max_lines: 50_000, # 50K events
2018
+ max_size: 50.megabytes # 50MB
2019
+ )
2020
+ ```
2021
+
2022
+ But note: **10K events is sufficient for 99% dev use cases**.
2023
+
2024
+ ### Q3: Can I use Web UI in production?
2025
+
2026
+ **A:** **NO!** Web UI is physically blocked in production:
2027
+ - `before_action :ensure_development_environment!` → raises error
2028
+ - dev_log adapter not registered in production
2029
+ - Web UI routes not mounted in production
2030
+
2031
+ For production → use Grafana + Loki.
2032
+
2033
+ ### Q4: What if I want to query Loki from Web UI?
2034
+
2035
+ **A:** Possible, but not recommended for v1.0:
2036
+
2037
+ ```ruby
2038
+ # lib/e11y/adapters/loki_query.rb (future feature)
2039
+ module E11y
2040
+ module Adapters
2041
+ class LokiQuery
2042
+ def all_events(limit: 1000)
2043
+ Loki.query('{app="myapp"}', limit: limit)
2044
+ end
2045
+ end
2046
+ end
2047
+ end
2048
+
2049
+ # Web UI controller (future)
2050
+ def index
2051
+ if params[:source] == 'loki'
2052
+ @events = E11y::Adapters::LokiQuery.new.all_events
2053
+ else
2054
+ @events = memory_adapter.all_events # Default
2055
+ end
2056
+ end
2057
+ ```
2058
+
2059
+ **Trade-off:**
2060
+ - ✅ Real production data
2061
+ - ❌ Requires Loki setup in dev
2062
+ - ❌ Slow queries (~500ms)
2063
+ - ❌ Complex auth
2064
+
2065
+ → **Decision:** Deferred to v1.1+ (YAGNI for v1.0)
2066
+
2067
+ ### Q5: How do events appear in Web UI?
2068
+
2069
+ **A:** Flow:
2070
+ ```
2071
+ 1. Events::OrderCreated.track(...)
2072
+ 2. → E11y Pipeline
2073
+ 3. → DevLog Adapter (if registered)
2074
+ 4. → adapter.send_batch([event])
2075
+ 5. → Append to log/e11y_dev.jsonl
2076
+ 6. → Web UI reads file (cached)
2077
+ ```
2078
+
2079
+ Web UI **does not create** events, it only **reads** from JSONL file.
2080
+
2081
+ ### Q6: Is it thread-safe and multi-process safe?
2082
+
2083
+ **A:** **Yes!** DevLog adapter uses:
2084
+ - Append-only writes (atomic on most filesystems)
2085
+ - File locking (`File::LOCK_EX`) during writes
2086
+ - Multi-process safe (shared file)
2087
+ - Thread-safe (mutex for writes)
2088
+
2089
+ Safe for multi-threaded servers (Puma, Falcon) and multi-process (Unicorn).
2090
+
2091
+ ### Q7: What if file grows too large during viewing?
2092
+
2093
+ **A:** Auto-rotation kicks in:
2094
+ ```ruby
2095
+ def rotate_if_needed!
2096
+ if File.size(@file_path) > @max_size
2097
+ # Keep last 50% of events
2098
+ rotate_file!
2099
+ end
2100
+ end
2101
+ ```
2102
+
2103
+ Rotation happens on next write, Web UI continues working with current file.
2104
+
2105
+ ### Q8: Can I use dev_log adapter in tests?
2106
+
2107
+ **A:** **Yes!** Auto-registered in test environment:
2108
+
2109
+ ```ruby
2110
+ # spec/support/e11y.rb
2111
+ RSpec.configure do |config|
2112
+ config.before(:each) do
2113
+ E11y.dev_log_adapter.clear! # Clear file before each test
2114
+ end
2115
+ end
2116
+
2117
+ # spec/features/order_spec.rb
2118
+ it 'tracks order creation' do
2119
+ visit new_order_path
2120
+ click_button 'Create Order'
2121
+
2122
+ events = E11y.dev_log_adapter.events_by_name('Events::OrderCreated')
2123
+ expect(events.size).to eq(1)
2124
+ expect(events.first[:payload][:order_id]).to be_present
2125
+ end
2126
+ ```
2127
+
2128
+ ### Q9: Can I export events from Web UI?
2129
+
2130
+ **A:** **Yes!**
2131
+ - JSON: `GET /e11y/events.json`
2132
+ - CSV: `GET /e11y/events.csv`
2133
+ - Copy-paste from UI
2134
+
2135
+ For automation:
2136
+ ```bash
2137
+ curl http://localhost:3000/e11y/events.json > events.json
2138
+ ```
2139
+
2140
+ ### Q10: How does debugging pipeline work with Web UI?
2141
+
2142
+ **A:** Combo approach:
2143
+ 1. **Console output** → immediate feedback (stdout)
2144
+ 2. **Web UI** → browse history, filter, inspect
2145
+ 3. **E11y.inspect** → trace pipeline (console)
2146
+
2147
+ ```ruby
2148
+ # Terminal: immediate feedback
2149
+ 18:42:31.123 INFO → Events::OrderCreated
2150
+
2151
+ # Browser: later inspection
2152
+ http://localhost:3000/e11y/events?event_name=Events::OrderCreated
2153
+
2154
+ # Console: deep dive
2155
+ >> E11y::Debug::PipelineInspector.trace_event(Events::OrderCreated, {...})
2156
+ ```
2157
+
2158
+ → **Best of both worlds!**
2159
+
2160
+ ---
2161
+
2162
+ **Status:** ✅ Complete with File-Based JSONL Architecture
2163
+ **Next:** ADR-007 (OpenTelemetry) or Begin Implementation
2164
+ **Estimated Implementation:** 1 week
2165
+
2166
+ **Key Takeaway:** File-based JSONL adapter = multi-process safe DX in development, with near-realtime updates and zero dependencies.