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