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,708 @@
|
|
|
1
|
+
# UC-020: Event Versioning & Schema Evolution
|
|
2
|
+
|
|
3
|
+
**Status:** Core Feature (MVP)
|
|
4
|
+
**Complexity:** Intermediate
|
|
5
|
+
**Setup Time:** 15-30 minutes
|
|
6
|
+
**Target Users:** Backend Developers, API Designers, Platform Engineers
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 📋 Overview
|
|
11
|
+
|
|
12
|
+
### Problem Statement
|
|
13
|
+
|
|
14
|
+
**Current Pain Points:**
|
|
15
|
+
|
|
16
|
+
1. **Breaking schema changes break production**
|
|
17
|
+
- Add required field → old code crashes
|
|
18
|
+
- Remove field → downstream consumers fail
|
|
19
|
+
- Rename field → data loss
|
|
20
|
+
|
|
21
|
+
2. **No backward compatibility**
|
|
22
|
+
- Can't deploy new event schema without coordinating all consumers
|
|
23
|
+
- Microservices must upgrade simultaneously (impossible!)
|
|
24
|
+
- Rollback is dangerous (data already sent with new schema)
|
|
25
|
+
|
|
26
|
+
3. **No schema evolution strategy**
|
|
27
|
+
- How to deprecate old fields?
|
|
28
|
+
- How to support multiple versions simultaneously?
|
|
29
|
+
- How to migrate consumers gradually?
|
|
30
|
+
|
|
31
|
+
### E11y Solution
|
|
32
|
+
|
|
33
|
+
**Event Versioning with Backward Compatibility:**
|
|
34
|
+
|
|
35
|
+
- Events have explicit version numbers
|
|
36
|
+
- Multiple versions can coexist
|
|
37
|
+
- Automatic version detection from payload
|
|
38
|
+
- Gradual migration path
|
|
39
|
+
|
|
40
|
+
**Result:** Safe schema evolution without breaking production.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🎯 Use Case Scenarios
|
|
45
|
+
|
|
46
|
+
### Scenario 1: Adding Required Field (Breaking Change)
|
|
47
|
+
|
|
48
|
+
**Problem:** Need to add required `currency` field to `OrderPaid` event.
|
|
49
|
+
|
|
50
|
+
**Without versioning (BREAKS PRODUCTION!):**
|
|
51
|
+
```ruby
|
|
52
|
+
class OrderPaid < E11y::Event::Base
|
|
53
|
+
schema do
|
|
54
|
+
required(:order_id).filled(:string)
|
|
55
|
+
required(:amount).filled(:decimal)
|
|
56
|
+
required(:currency).filled(:string) # ← NEW! Breaks old code!
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Old code (deployed):
|
|
61
|
+
OrderPaid.track(order_id: '123', amount: 99.99)
|
|
62
|
+
# ❌ ValidationError: currency is required
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**With versioning (SAFE!):**
|
|
66
|
+
```ruby
|
|
67
|
+
# V1: Original version (no version suffix!)
|
|
68
|
+
class OrderPaid < E11y::Event::Base
|
|
69
|
+
version 1 # Optional for v1, but recommended
|
|
70
|
+
event_name 'order.paid'
|
|
71
|
+
|
|
72
|
+
schema do
|
|
73
|
+
required(:order_id).filled(:string)
|
|
74
|
+
required(:amount).filled(:decimal)
|
|
75
|
+
# No currency (backward compatible)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# V2: New version with currency (suffix V2)
|
|
80
|
+
class OrderPaidV2 < E11y::Event::Base
|
|
81
|
+
version 2
|
|
82
|
+
event_name 'order.paid'
|
|
83
|
+
|
|
84
|
+
schema do
|
|
85
|
+
required(:order_id).filled(:string)
|
|
86
|
+
required(:amount).filled(:decimal)
|
|
87
|
+
required(:currency).filled(:string) # ← New required field
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Old code (still deployed):
|
|
92
|
+
OrderPaid.track(order_id: '123', amount: 99.99)
|
|
93
|
+
# ✅ Works! Sends version: 1
|
|
94
|
+
|
|
95
|
+
# New code (gradual rollout):
|
|
96
|
+
OrderPaidV2.track(order_id: '123', amount: 99.99, currency: 'USD')
|
|
97
|
+
# ✅ Works! Sends version: 2
|
|
98
|
+
|
|
99
|
+
# Downstream consumers can handle both versions!
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### Scenario 2: Renaming Field (Breaking Change)
|
|
105
|
+
|
|
106
|
+
**Problem:** Need to rename `user_id` → `customer_id` for consistency.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# V1: Original version (no suffix)
|
|
110
|
+
class UserSignup < E11y::Event::Base
|
|
111
|
+
version 1
|
|
112
|
+
event_name 'user.signup'
|
|
113
|
+
|
|
114
|
+
schema do
|
|
115
|
+
required(:user_id).filled(:string) # ← Old name
|
|
116
|
+
required(:email).filled(:string)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Optional: Auto-map to V2 format
|
|
120
|
+
def to_v2
|
|
121
|
+
UserSignupV2.new(
|
|
122
|
+
customer_id: payload[:user_id], # Map old → new
|
|
123
|
+
email: payload[:email]
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# V2: New field name (V2 suffix)
|
|
129
|
+
class UserSignupV2 < E11y::Event::Base
|
|
130
|
+
version 2
|
|
131
|
+
event_name 'user.signup'
|
|
132
|
+
|
|
133
|
+
schema do
|
|
134
|
+
required(:customer_id).filled(:string) # ← Renamed
|
|
135
|
+
required(:email).filled(:string)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Migration path:
|
|
140
|
+
# 1. Deploy V2 event class (both versions coexist)
|
|
141
|
+
# 2. Update tracking calls gradually (service by service)
|
|
142
|
+
# 3. Monitor: no more V1 events for 30 days
|
|
143
|
+
# 4. Deprecate V1 class
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Scenario 3: Removing Field (Breaking Change)
|
|
149
|
+
|
|
150
|
+
**Problem:** Remove sensitive field that shouldn't have been logged.
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# V1: Old version with sensitive field (DEPRECATED)
|
|
154
|
+
class PaymentProcessed < E11y::Event::Base
|
|
155
|
+
version 1
|
|
156
|
+
event_name 'payment.processed'
|
|
157
|
+
deprecated true # Mark as deprecated
|
|
158
|
+
deprecation_warning 'Use PaymentProcessedV2. V1 will be removed 2026-06-01'
|
|
159
|
+
|
|
160
|
+
schema do
|
|
161
|
+
required(:transaction_id).filled(:string)
|
|
162
|
+
required(:amount).filled(:decimal)
|
|
163
|
+
optional(:card_number).filled(:string) # ← SECURITY ISSUE!
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Emit deprecation warning
|
|
167
|
+
after_track do |event|
|
|
168
|
+
Rails.logger.warn "DEPRECATED: PaymentProcessed (v1). Use PaymentProcessedV2."
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# V2: Removed sensitive field (V2 suffix)
|
|
173
|
+
class PaymentProcessedV2 < E11y::Event::Base
|
|
174
|
+
version 2
|
|
175
|
+
event_name 'payment.processed'
|
|
176
|
+
|
|
177
|
+
schema do
|
|
178
|
+
required(:transaction_id).filled(:string)
|
|
179
|
+
required(:amount).filled(:decimal)
|
|
180
|
+
# card_number REMOVED (was security issue!)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 🏗️ Architecture
|
|
188
|
+
|
|
189
|
+
> **Implementation:** See [ADR-012: Event Evolution](../ADR-012-event-evolution.md) for complete versioning architecture, including [Section 3: Naming Convention](../ADR-012-event-evolution.md#3-naming-convention), [Section 4: Version in Payload](../ADR-012-event-evolution.md#4-version-in-payload), [Section 6: Event Registry Integration](../ADR-012-event-evolution.md#6-event-registry-integration), and [Section 7: Migration Strategy](../ADR-012-event-evolution.md#7-migration-strategy).
|
|
190
|
+
|
|
191
|
+
### Version Management
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
195
|
+
│ Event Registry (tracks all versions) │
|
|
196
|
+
│ │
|
|
197
|
+
│ event_name: 'order.paid' │
|
|
198
|
+
│ ├─ V1: OrderPaidV1 │
|
|
199
|
+
│ ├─ V2: OrderPaidV2 (current) │
|
|
200
|
+
│ └─ V3: OrderPaidV3 (future) │
|
|
201
|
+
│ │
|
|
202
|
+
│ current_version: 2 │
|
|
203
|
+
│ deprecated_versions: [1] │
|
|
204
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
205
|
+
|
|
206
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
207
|
+
│ Event Payload (includes version) │
|
|
208
|
+
│ │
|
|
209
|
+
│ { │
|
|
210
|
+
│ "@timestamp": "2026-01-12T10:30:00Z", │
|
|
211
|
+
│ "event_name": "order.paid", │
|
|
212
|
+
│ "event_version": 2, ← Version included! │
|
|
213
|
+
│ "payload": { │
|
|
214
|
+
│ "order_id": "123", │
|
|
215
|
+
│ "amount": 99.99, │
|
|
216
|
+
│ "currency": "USD" ← New field in V2 │
|
|
217
|
+
│ } │
|
|
218
|
+
│ } │
|
|
219
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 🔧 Configuration
|
|
225
|
+
|
|
226
|
+
### Basic Setup
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# config/initializers/e11y.rb
|
|
230
|
+
E11y.configure do |config|
|
|
231
|
+
config.versioning do
|
|
232
|
+
enabled true
|
|
233
|
+
|
|
234
|
+
# Include version in event payload
|
|
235
|
+
include_version_in_payload true
|
|
236
|
+
version_field :event_version # Field name
|
|
237
|
+
|
|
238
|
+
# Deprecation warnings
|
|
239
|
+
warn_on_deprecated_version true
|
|
240
|
+
deprecation_log_level :warn # :info, :warn, :error
|
|
241
|
+
|
|
242
|
+
# Automatic version detection
|
|
243
|
+
auto_detect_version true # From payload structure
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Advanced: Version Migration
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
E11y.configure do |config|
|
|
252
|
+
config.versioning do
|
|
253
|
+
enabled true
|
|
254
|
+
|
|
255
|
+
# Auto-upgrade old versions
|
|
256
|
+
auto_upgrade_to_current do
|
|
257
|
+
enabled false # Disabled by default (explicit migration)
|
|
258
|
+
|
|
259
|
+
# If enabled, V1 events auto-converted to V2
|
|
260
|
+
upgrade 'order.paid' do
|
|
261
|
+
from_version 1
|
|
262
|
+
to_version 2
|
|
263
|
+
|
|
264
|
+
transform do |v1_payload|
|
|
265
|
+
v2_payload = v1_payload.dup
|
|
266
|
+
v2_payload[:currency] = 'USD' # Add default for missing field
|
|
267
|
+
v2_payload
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Deprecation enforcement
|
|
273
|
+
deprecation_enforcement do
|
|
274
|
+
# After this date, V1 events rejected
|
|
275
|
+
enforce_after '2026-06-01'
|
|
276
|
+
|
|
277
|
+
# What to do with deprecated versions after enforce_after
|
|
278
|
+
on_deprecated_version :reject # :reject, :warn, :upgrade
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 📝 Event Definition Examples
|
|
287
|
+
|
|
288
|
+
### Example 1: Simple Versioning
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
# app/events/order_paid.rb (V1 - no suffix!)
|
|
292
|
+
module Events
|
|
293
|
+
class OrderPaid < E11y::Event::Base
|
|
294
|
+
version 1 # Explicit version (recommended)
|
|
295
|
+
event_name 'order.paid'
|
|
296
|
+
|
|
297
|
+
schema do
|
|
298
|
+
required(:order_id).filled(:string)
|
|
299
|
+
required(:amount).filled(:decimal)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# app/events/order_paid_v2.rb (V2+ - with suffix!)
|
|
305
|
+
module Events
|
|
306
|
+
class OrderPaidV2 < E11y::Event::Base
|
|
307
|
+
version 2
|
|
308
|
+
event_name 'order.paid'
|
|
309
|
+
|
|
310
|
+
schema do
|
|
311
|
+
required(:order_id).filled(:string)
|
|
312
|
+
required(:amount).filled(:decimal)
|
|
313
|
+
required(:currency).filled(:string) # New required field
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Default version (latest)
|
|
317
|
+
default_version true
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Usage:
|
|
322
|
+
# Old code:
|
|
323
|
+
Events::OrderPaid.track(order_id: '123', amount: 99.99) # V1
|
|
324
|
+
|
|
325
|
+
# New code:
|
|
326
|
+
Events::OrderPaidV2.track(order_id: '123', amount: 99.99, currency: 'USD') # V2
|
|
327
|
+
|
|
328
|
+
# Or use version-agnostic routing (requires config):
|
|
329
|
+
E11y::Registry.track('order.paid', order_id: '123', ...) # → Routes to default_version (V2)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Example 2: With Deprecation
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
module Events
|
|
336
|
+
# V1 (no suffix, but deprecated)
|
|
337
|
+
class UserLogin < E11y::Event::Base
|
|
338
|
+
version 1
|
|
339
|
+
event_name 'user.login'
|
|
340
|
+
|
|
341
|
+
# Mark as deprecated
|
|
342
|
+
deprecated true
|
|
343
|
+
deprecation_date '2026-03-01'
|
|
344
|
+
deprecation_message 'Use UserLoginV2 with ip_address field'
|
|
345
|
+
|
|
346
|
+
schema do
|
|
347
|
+
required(:user_id).filled(:string)
|
|
348
|
+
required(:success).filled(:bool)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Emit warning on each track
|
|
352
|
+
after_track do |event|
|
|
353
|
+
Rails.logger.warn "[DEPRECATED] UserLogin (v1) used. Migrate to V2 by 2026-03-01"
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# V2+ (with suffix)
|
|
358
|
+
class UserLoginV2 < E11y::Event::Base
|
|
359
|
+
version 2
|
|
360
|
+
event_name 'user.login'
|
|
361
|
+
default_version true
|
|
362
|
+
|
|
363
|
+
schema do
|
|
364
|
+
required(:user_id).filled(:string)
|
|
365
|
+
required(:success).filled(:bool)
|
|
366
|
+
required(:ip_address).filled(:string) # New security requirement
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Example 3: With Auto-Migration
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
module Events
|
|
376
|
+
# V1 (no suffix)
|
|
377
|
+
class OrderShipped < E11y::Event::Base
|
|
378
|
+
version 1
|
|
379
|
+
event_name 'order.shipped'
|
|
380
|
+
|
|
381
|
+
schema do
|
|
382
|
+
required(:order_id).filled(:string)
|
|
383
|
+
required(:tracking_number).filled(:string)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Define migration to V2
|
|
387
|
+
def migrate_to_v2
|
|
388
|
+
OrderShippedV2.new(
|
|
389
|
+
order_id: payload[:order_id],
|
|
390
|
+
tracking_number: payload[:tracking_number],
|
|
391
|
+
carrier: 'USPS' # Default for old events
|
|
392
|
+
)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# V2+ (with suffix)
|
|
397
|
+
class OrderShippedV2 < E11y::Event::Base
|
|
398
|
+
version 2
|
|
399
|
+
event_name 'order.shipped'
|
|
400
|
+
default_version true
|
|
401
|
+
|
|
402
|
+
schema do
|
|
403
|
+
required(:order_id).filled(:string)
|
|
404
|
+
required(:tracking_number).filled(:string)
|
|
405
|
+
required(:carrier).filled(:string) # New required field
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Configuration for auto-migration:
|
|
411
|
+
E11y.configure do |config|
|
|
412
|
+
config.versioning.auto_upgrade do
|
|
413
|
+
upgrade 'order.shipped' do
|
|
414
|
+
from_version 1
|
|
415
|
+
to_version 2
|
|
416
|
+
transform_method :migrate_to_v2 # Call event.migrate_to_v2
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 💡 Best Practices
|
|
425
|
+
|
|
426
|
+
### ✅ DO
|
|
427
|
+
|
|
428
|
+
**1. Always increment version for breaking changes**
|
|
429
|
+
```ruby
|
|
430
|
+
# ✅ GOOD: New version for breaking change
|
|
431
|
+
class OrderPaidV2 < E11y::Event::Base
|
|
432
|
+
version 2 # ← Incremented
|
|
433
|
+
event_name 'order.paid'
|
|
434
|
+
|
|
435
|
+
schema do
|
|
436
|
+
required(:currency).filled(:string) # ← New required field
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**2. Keep old versions for backward compatibility**
|
|
442
|
+
```ruby
|
|
443
|
+
# ✅ GOOD: Keep V1 around during migration
|
|
444
|
+
class OrderPaid < E11y::Event::Base # V1 (no suffix)
|
|
445
|
+
version 1
|
|
446
|
+
deprecated true # Mark as deprecated
|
|
447
|
+
deprecation_date '2026-06-01'
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
class OrderPaidV2 < E11y::Event::Base # V2+ (with suffix)
|
|
451
|
+
version 2
|
|
452
|
+
default_version true
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Remove OrderPaid (v1) after deprecation_date + grace period
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**3. Document breaking changes**
|
|
459
|
+
```ruby
|
|
460
|
+
# ✅ GOOD: Clear documentation
|
|
461
|
+
class PaymentProcessedV2 < E11y::Event::Base
|
|
462
|
+
version 2
|
|
463
|
+
|
|
464
|
+
# BREAKING CHANGES from V1:
|
|
465
|
+
# - Removed: card_number (security)
|
|
466
|
+
# - Added: payment_method_id (reference)
|
|
467
|
+
# - Renamed: user_id → customer_id
|
|
468
|
+
|
|
469
|
+
schema do
|
|
470
|
+
required(:customer_id).filled(:string) # Was: user_id
|
|
471
|
+
required(:payment_method_id).filled(:string) # New
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**4. Use semantic versioning for major changes**
|
|
477
|
+
```ruby
|
|
478
|
+
# ✅ GOOD: Major version for major changes
|
|
479
|
+
class OrderPaidV1 < E11y::Event::Base
|
|
480
|
+
version 1 # Initial version
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
class OrderPaidV2 < E11y::Event::Base
|
|
484
|
+
version 2 # Added currency field
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
class OrderPaidV3 < E11y::Event::Base
|
|
488
|
+
version 3 # Restructured to support multi-currency
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
### ❌ DON'T
|
|
495
|
+
|
|
496
|
+
**1. Don't change schema without versioning**
|
|
497
|
+
```ruby
|
|
498
|
+
# ❌ BAD: Changed schema without version increment
|
|
499
|
+
class OrderPaid < E11y::Event::Base
|
|
500
|
+
schema do
|
|
501
|
+
required(:order_id).filled(:string)
|
|
502
|
+
required(:amount).filled(:decimal)
|
|
503
|
+
required(:currency).filled(:string) # ← Added without version++
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
# This BREAKS old code in production!
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**2. Don't delete old versions prematurely**
|
|
510
|
+
```ruby
|
|
511
|
+
# ❌ BAD: Deleted V1 (OrderPaid) immediately
|
|
512
|
+
# Old services still sending V1 events → errors!
|
|
513
|
+
|
|
514
|
+
# ✅ GOOD: Deprecate first, delete after grace period
|
|
515
|
+
class OrderPaid < E11y::Event::Base # V1
|
|
516
|
+
deprecated true
|
|
517
|
+
deprecation_date '2026-06-01'
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Monitor for 30 days after deprecation_date
|
|
521
|
+
# Delete OrderPaid (v1) class only when no more V1 events tracked
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**3. Don't use version for non-breaking changes**
|
|
525
|
+
```ruby
|
|
526
|
+
# ❌ BAD: Version increment for optional field
|
|
527
|
+
class OrderPaidV2 < E11y::Event::Base
|
|
528
|
+
version 2 # ← Unnecessary!
|
|
529
|
+
|
|
530
|
+
schema do
|
|
531
|
+
required(:order_id).filled(:string)
|
|
532
|
+
optional(:notes).filled(:string) # ← Optional = not breaking!
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# ✅ GOOD: Just add optional field to existing version
|
|
537
|
+
class OrderPaid < E11y::Event::Base
|
|
538
|
+
version 1 # Same version
|
|
539
|
+
|
|
540
|
+
schema do
|
|
541
|
+
required(:order_id).filled(:string)
|
|
542
|
+
optional(:notes).filled(:string) # ← Optional = backward compatible
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## 🎯 Migration Strategy
|
|
550
|
+
|
|
551
|
+
### Phase 1: Deploy New Version (Coexistence)
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
# Week 1: Deploy both versions
|
|
555
|
+
# - V1 still works (backward compatible)
|
|
556
|
+
# - V2 available for new code
|
|
557
|
+
|
|
558
|
+
# app/events/order_paid.rb (existing V1 - no suffix)
|
|
559
|
+
class OrderPaid < E11y::Event::Base
|
|
560
|
+
version 1
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# app/events/order_paid_v2.rb (new V2 - with suffix)
|
|
564
|
+
class OrderPaidV2 < E11y::Event::Base
|
|
565
|
+
version 2
|
|
566
|
+
default_version true # New default
|
|
567
|
+
end
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Phase 2: Gradual Migration
|
|
571
|
+
|
|
572
|
+
```ruby
|
|
573
|
+
# Week 2-4: Migrate services one by one
|
|
574
|
+
|
|
575
|
+
# Service A (updated):
|
|
576
|
+
Events::OrderPaidV2.track(order_id: '123', amount: 99.99, currency: 'USD')
|
|
577
|
+
|
|
578
|
+
# Service B (not updated yet):
|
|
579
|
+
Events::OrderPaid.track(order_id: '456', amount: 49.99) # V1 still works!
|
|
580
|
+
|
|
581
|
+
# Monitor:
|
|
582
|
+
# - % of V1 vs V2 events
|
|
583
|
+
# - Which services still use V1
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Phase 3: Deprecation Warning
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
# Week 5: Mark V1 as deprecated
|
|
590
|
+
class OrderPaid < E11y::Event::Base # V1
|
|
591
|
+
version 1
|
|
592
|
+
deprecated true
|
|
593
|
+
deprecation_date '2026-06-01' # 30 days from now
|
|
594
|
+
|
|
595
|
+
after_track do |event|
|
|
596
|
+
# Emit warning
|
|
597
|
+
Rails.logger.warn "DEPRECATED: OrderPaid (v1). Migrate by 2026-06-01"
|
|
598
|
+
|
|
599
|
+
# Track deprecation usage
|
|
600
|
+
Events::DeprecatedEventUsed.track(
|
|
601
|
+
event_class: 'OrderPaid',
|
|
602
|
+
event_version: 1,
|
|
603
|
+
service: ENV['SERVICE_NAME']
|
|
604
|
+
)
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Phase 4: Enforcement
|
|
610
|
+
|
|
611
|
+
```ruby
|
|
612
|
+
# After 2026-06-01: Reject V1 events
|
|
613
|
+
E11y.configure do |config|
|
|
614
|
+
config.versioning.deprecation_enforcement do
|
|
615
|
+
enforce_after '2026-06-01'
|
|
616
|
+
on_deprecated_version :reject # Reject V1 events
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
# Or auto-upgrade:
|
|
621
|
+
E11y.configure do |config|
|
|
622
|
+
config.versioning.auto_upgrade do
|
|
623
|
+
upgrade 'order.paid' do
|
|
624
|
+
from_version 1
|
|
625
|
+
to_version 2
|
|
626
|
+
transform { |v1| v1.merge(currency: 'USD') } # Default currency
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Phase 5: Cleanup
|
|
633
|
+
|
|
634
|
+
```ruby
|
|
635
|
+
# 30 days after enforcement: Delete V1 class
|
|
636
|
+
# 1. Verify zero V1 events in last 30 days
|
|
637
|
+
# 2. Remove OrderPaid (v1) class file: app/events/order_paid.rb
|
|
638
|
+
# 3. Rename OrderPaidV2 → OrderPaid (optional, for next v3 migration)
|
|
639
|
+
# 4. Update documentation
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 📊 Monitoring & Metrics
|
|
645
|
+
|
|
646
|
+
### Version Usage Metrics
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
# E11y automatically tracks version usage
|
|
650
|
+
E11y.metrics do
|
|
651
|
+
counter :events_by_version_total,
|
|
652
|
+
tags: [:event_name, :version],
|
|
653
|
+
comment: 'Events tracked by version'
|
|
654
|
+
|
|
655
|
+
gauge :deprecated_events_active,
|
|
656
|
+
tags: [:event_name, :version],
|
|
657
|
+
comment: 'Deprecated versions still in use'
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Prometheus queries:
|
|
661
|
+
# events_by_version_total{event_name="order.paid", version="1"}
|
|
662
|
+
# events_by_version_total{event_name="order.paid", version="2"}
|
|
663
|
+
|
|
664
|
+
# Alert when deprecated version usage > 0 after deprecation_date
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### Deprecation Dashboard
|
|
668
|
+
|
|
669
|
+
```ruby
|
|
670
|
+
# Grafana dashboard queries:
|
|
671
|
+
|
|
672
|
+
# % of events by version
|
|
673
|
+
sum(rate(events_by_version_total{event_name="order.paid"}[5m])) by (version)
|
|
674
|
+
|
|
675
|
+
# Services still using deprecated versions
|
|
676
|
+
sum(deprecated_events_active) by (service, event_name, version)
|
|
677
|
+
|
|
678
|
+
# Days until deprecation enforcement
|
|
679
|
+
(deprecation_date - now()) / 86400
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## 🔗 Related Use Cases
|
|
685
|
+
|
|
686
|
+
- **[UC-002: Business Event Tracking](./UC-002-business-event-tracking.md)** - Event schema definition
|
|
687
|
+
- **[UC-018: Testing Events](./UC-018-testing-events.md)** - Testing versioned events
|
|
688
|
+
- **[UC-016: Rails Logger Migration](./UC-016-rails-logger-migration.md)** - Migration strategies
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## 🚀 Quick Start Checklist
|
|
693
|
+
|
|
694
|
+
- [ ] Enable versioning in config
|
|
695
|
+
- [ ] Define V2 event class with new schema
|
|
696
|
+
- [ ] Keep V1 event class (backward compatible)
|
|
697
|
+
- [ ] Update tracking calls gradually
|
|
698
|
+
- [ ] Monitor version usage metrics
|
|
699
|
+
- [ ] Mark V1 as deprecated
|
|
700
|
+
- [ ] Set deprecation_date
|
|
701
|
+
- [ ] Enforce after grace period
|
|
702
|
+
- [ ] Delete old version after 30 days no usage
|
|
703
|
+
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
**Status:** ✅ Core Feature
|
|
707
|
+
**Priority:** High (schema evolution is critical)
|
|
708
|
+
**Complexity:** Intermediate
|