e11y 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +69 -0
  4. data/CHANGELOG.md +26 -0
  5. data/CODE_OF_CONDUCT.md +64 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +179 -0
  8. data/Rakefile +37 -0
  9. data/benchmarks/run_all.rb +33 -0
  10. data/config/README.md +83 -0
  11. data/config/loki-local-config.yaml +35 -0
  12. data/config/prometheus.yml +15 -0
  13. data/docker-compose.yml +78 -0
  14. data/docs/00-ICP-AND-TIMELINE.md +483 -0
  15. data/docs/01-SCALE-REQUIREMENTS.md +858 -0
  16. data/docs/ADR-001-architecture.md +2617 -0
  17. data/docs/ADR-002-metrics-yabeda.md +1395 -0
  18. data/docs/ADR-003-slo-observability.md +3337 -0
  19. data/docs/ADR-004-adapter-architecture.md +2385 -0
  20. data/docs/ADR-005-tracing-context.md +1372 -0
  21. data/docs/ADR-006-security-compliance.md +4143 -0
  22. data/docs/ADR-007-opentelemetry-integration.md +1385 -0
  23. data/docs/ADR-008-rails-integration.md +1911 -0
  24. data/docs/ADR-009-cost-optimization.md +2993 -0
  25. data/docs/ADR-010-developer-experience.md +2166 -0
  26. data/docs/ADR-011-testing-strategy.md +1836 -0
  27. data/docs/ADR-012-event-evolution.md +958 -0
  28. data/docs/ADR-013-reliability-error-handling.md +2750 -0
  29. data/docs/ADR-014-event-driven-slo.md +1533 -0
  30. data/docs/ADR-015-middleware-order.md +1061 -0
  31. data/docs/ADR-016-self-monitoring-slo.md +1234 -0
  32. data/docs/API-REFERENCE-L28.md +914 -0
  33. data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
  34. data/docs/IMPLEMENTATION_NOTES.md +2804 -0
  35. data/docs/IMPLEMENTATION_PLAN.md +1971 -0
  36. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
  37. data/docs/PLAN.md +148 -0
  38. data/docs/QUICK-START.md +934 -0
  39. data/docs/README.md +296 -0
  40. data/docs/design/00-memory-optimization.md +593 -0
  41. data/docs/guides/MIGRATION-L27-L28.md +692 -0
  42. data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
  43. data/docs/guides/README.md +44 -0
  44. data/docs/prd/01-overview-vision.md +440 -0
  45. data/docs/use_cases/README.md +119 -0
  46. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
  47. data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
  48. data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
  49. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
  50. data/docs/use_cases/UC-005-sentry-integration.md +759 -0
  51. data/docs/use_cases/UC-006-trace-context-management.md +905 -0
  52. data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
  53. data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
  54. data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
  55. data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
  56. data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
  57. data/docs/use_cases/UC-012-audit-trail.md +2301 -0
  58. data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
  59. data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
  60. data/docs/use_cases/UC-015-cost-optimization.md +735 -0
  61. data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
  62. data/docs/use_cases/UC-017-local-development.md +867 -0
  63. data/docs/use_cases/UC-018-testing-events.md +1081 -0
  64. data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
  65. data/docs/use_cases/UC-020-event-versioning.md +708 -0
  66. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
  67. data/docs/use_cases/UC-022-event-registry.md +648 -0
  68. data/docs/use_cases/backlog.md +226 -0
  69. data/e11y.gemspec +76 -0
  70. data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
  71. data/lib/e11y/adapters/audit_encrypted.rb +239 -0
  72. data/lib/e11y/adapters/base.rb +580 -0
  73. data/lib/e11y/adapters/file.rb +224 -0
  74. data/lib/e11y/adapters/in_memory.rb +216 -0
  75. data/lib/e11y/adapters/loki.rb +333 -0
  76. data/lib/e11y/adapters/otel_logs.rb +203 -0
  77. data/lib/e11y/adapters/registry.rb +141 -0
  78. data/lib/e11y/adapters/sentry.rb +230 -0
  79. data/lib/e11y/adapters/stdout.rb +108 -0
  80. data/lib/e11y/adapters/yabeda.rb +370 -0
  81. data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
  82. data/lib/e11y/buffers/base_buffer.rb +40 -0
  83. data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
  84. data/lib/e11y/buffers/ring_buffer.rb +267 -0
  85. data/lib/e11y/buffers.rb +14 -0
  86. data/lib/e11y/console.rb +122 -0
  87. data/lib/e11y/current.rb +48 -0
  88. data/lib/e11y/event/base.rb +894 -0
  89. data/lib/e11y/event/value_sampling_config.rb +84 -0
  90. data/lib/e11y/events/base_audit_event.rb +43 -0
  91. data/lib/e11y/events/base_payment_event.rb +33 -0
  92. data/lib/e11y/events/rails/cache/delete.rb +21 -0
  93. data/lib/e11y/events/rails/cache/read.rb +23 -0
  94. data/lib/e11y/events/rails/cache/write.rb +22 -0
  95. data/lib/e11y/events/rails/database/query.rb +45 -0
  96. data/lib/e11y/events/rails/http/redirect.rb +21 -0
  97. data/lib/e11y/events/rails/http/request.rb +26 -0
  98. data/lib/e11y/events/rails/http/send_file.rb +21 -0
  99. data/lib/e11y/events/rails/http/start_processing.rb +26 -0
  100. data/lib/e11y/events/rails/job/completed.rb +22 -0
  101. data/lib/e11y/events/rails/job/enqueued.rb +22 -0
  102. data/lib/e11y/events/rails/job/failed.rb +22 -0
  103. data/lib/e11y/events/rails/job/scheduled.rb +23 -0
  104. data/lib/e11y/events/rails/job/started.rb +22 -0
  105. data/lib/e11y/events/rails/log.rb +56 -0
  106. data/lib/e11y/events/rails/view/render.rb +23 -0
  107. data/lib/e11y/events.rb +18 -0
  108. data/lib/e11y/instruments/active_job.rb +201 -0
  109. data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
  110. data/lib/e11y/instruments/sidekiq.rb +175 -0
  111. data/lib/e11y/logger/bridge.rb +205 -0
  112. data/lib/e11y/metrics/cardinality_protection.rb +172 -0
  113. data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
  114. data/lib/e11y/metrics/registry.rb +234 -0
  115. data/lib/e11y/metrics/relabeling.rb +226 -0
  116. data/lib/e11y/metrics.rb +102 -0
  117. data/lib/e11y/middleware/audit_signing.rb +174 -0
  118. data/lib/e11y/middleware/base.rb +140 -0
  119. data/lib/e11y/middleware/event_slo.rb +167 -0
  120. data/lib/e11y/middleware/pii_filter.rb +266 -0
  121. data/lib/e11y/middleware/pii_filtering.rb +280 -0
  122. data/lib/e11y/middleware/rate_limiting.rb +214 -0
  123. data/lib/e11y/middleware/request.rb +163 -0
  124. data/lib/e11y/middleware/routing.rb +157 -0
  125. data/lib/e11y/middleware/sampling.rb +254 -0
  126. data/lib/e11y/middleware/slo.rb +168 -0
  127. data/lib/e11y/middleware/trace_context.rb +131 -0
  128. data/lib/e11y/middleware/validation.rb +118 -0
  129. data/lib/e11y/middleware/versioning.rb +132 -0
  130. data/lib/e11y/middleware.rb +12 -0
  131. data/lib/e11y/pii/patterns.rb +90 -0
  132. data/lib/e11y/pii.rb +13 -0
  133. data/lib/e11y/pipeline/builder.rb +155 -0
  134. data/lib/e11y/pipeline/zone_validator.rb +110 -0
  135. data/lib/e11y/pipeline.rb +12 -0
  136. data/lib/e11y/presets/audit_event.rb +65 -0
  137. data/lib/e11y/presets/debug_event.rb +34 -0
  138. data/lib/e11y/presets/high_value_event.rb +51 -0
  139. data/lib/e11y/presets.rb +19 -0
  140. data/lib/e11y/railtie.rb +138 -0
  141. data/lib/e11y/reliability/circuit_breaker.rb +216 -0
  142. data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
  143. data/lib/e11y/reliability/dlq/filter.rb +117 -0
  144. data/lib/e11y/reliability/retry_handler.rb +207 -0
  145. data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
  146. data/lib/e11y/sampling/error_spike_detector.rb +225 -0
  147. data/lib/e11y/sampling/load_monitor.rb +161 -0
  148. data/lib/e11y/sampling/stratified_tracker.rb +92 -0
  149. data/lib/e11y/sampling/value_extractor.rb +82 -0
  150. data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
  151. data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
  152. data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
  153. data/lib/e11y/slo/event_driven.rb +150 -0
  154. data/lib/e11y/slo/tracker.rb +119 -0
  155. data/lib/e11y/version.rb +9 -0
  156. data/lib/e11y.rb +283 -0
  157. metadata +452 -0
@@ -0,0 +1,1953 @@
1
+ # UC-002: Business Event Tracking
2
+
3
+ **Status:** Core Feature (MVP)
4
+ **Complexity:** Beginner
5
+ **Setup Time:** 5-15 minutes
6
+ **Target Users:** Ruby/Rails Developers
7
+
8
+ ---
9
+
10
+ ## 📋 Overview
11
+
12
+ ### Problem Statement
13
+
14
+ **Current Approach (Rails.logger):**
15
+ ```ruby
16
+ # ❌ Unstructured, hard to query
17
+ Rails.logger.info "Order 123 paid $99.99 USD via stripe"
18
+
19
+ # ❌ Manual metrics tracking (duplication)
20
+ Rails.logger.info "Order paid: #{order.id}"
21
+ OrderMetrics.increment('orders.paid.total')
22
+ OrderMetrics.observe('orders.paid.amount', order.amount)
23
+
24
+ # Problems:
25
+ # - Free-form text → hard to parse/query
26
+ # - Manual metrics → boilerplate + bugs
27
+ # - No schema → typos, inconsistencies
28
+ # - No type safety → runtime errors
29
+ ```
30
+
31
+ ### E11y Solution
32
+
33
+ **Structured events with automatic metrics:**
34
+ ```ruby
35
+ # ✅ Type-safe, structured, queryable
36
+ Events::OrderPaid.track(
37
+ order_id: '123',
38
+ amount: 99.99,
39
+ currency: 'USD',
40
+ payment_method: 'stripe'
41
+ )
42
+
43
+ # Result:
44
+ # 1. Structured log in ELK/Loki (JSON)
45
+ # 2. Auto-generated metrics (pattern-based)
46
+ # 3. Trace context (automatic correlation)
47
+ ```
48
+
49
+ ---
50
+
51
+ ## 🎯 Use Case Scenarios
52
+
53
+ ### Scenario 1: E-Commerce Order Flow
54
+
55
+ **Business Events:**
56
+ 1. Order Created
57
+ 2. Order Paid
58
+ 3. Order Shipped
59
+ 4. Order Delivered
60
+
61
+ **Implementation:**
62
+
63
+ ```ruby
64
+ # Step 1: Define events (app/events/order_created.rb)
65
+ module Events
66
+ class OrderCreated < E11y::Event::Base
67
+ # Schema
68
+ schema do
69
+ required(:order_id).filled(:string)
70
+ required(:user_id).filled(:string)
71
+ required(:items_count).filled(:integer)
72
+ required(:total_amount).filled(:decimal)
73
+ optional(:currency).filled(:string)
74
+ end
75
+
76
+ # Default severity
77
+ severity :success
78
+
79
+ # Adapters (optional override)
80
+ # If not specified, uses global config.adapters
81
+ # adapters [
82
+ # E11y::Adapters::LokiAdapter.new(...),
83
+ # E11y::Adapters::SentryAdapter.new(...)
84
+ # ]
85
+ end
86
+ end
87
+
88
+ # Step 2: Track events in controller
89
+ class OrdersController < ApplicationController
90
+ def create
91
+ order = CreateOrderService.call(params)
92
+
93
+ Events::OrderCreated.track(
94
+ order_id: order.id,
95
+ user_id: current_user.id,
96
+ items_count: order.items.count,
97
+ total_amount: order.total,
98
+ currency: order.currency
99
+ )
100
+
101
+ render json: order
102
+ end
103
+ end
104
+
105
+ # Step 3: Configure pattern-based metrics (config/initializers/e11y.rb)
106
+ E11y.configure do |config|
107
+ config.metrics do
108
+ # Counter: orders.created.total
109
+ counter_for pattern: 'order.created',
110
+ name: 'orders.created.total',
111
+ tags: [:currency]
112
+
113
+ # Histogram: orders.created.amount
114
+ histogram_for pattern: 'order.created',
115
+ name: 'orders.created.amount',
116
+ value: ->(e) { e.payload[:total_amount] },
117
+ tags: [:currency],
118
+ buckets: [10, 50, 100, 500, 1000, 5000]
119
+ end
120
+ end
121
+ ```
122
+
123
+ **Result in Logs (Loki/ELK):**
124
+ ```json
125
+ {
126
+ "timestamp": "2026-01-12T10:30:00Z",
127
+ "event_name": "order.created",
128
+ "severity": "success",
129
+ "trace_id": "abc-123-def",
130
+ "user_id": "user_456",
131
+ "payload": {
132
+ "order_id": "ORD-789",
133
+ "user_id": "user_456",
134
+ "items_count": 3,
135
+ "total_amount": 299.97,
136
+ "currency": "USD"
137
+ },
138
+ "context": {
139
+ "env": "production",
140
+ "service": "api",
141
+ "host": "web-1"
142
+ }
143
+ }
144
+ ```
145
+
146
+ **Result in Metrics (Prometheus):**
147
+ ```promql
148
+ # Counter
149
+ orders_created_total{currency="USD"} 1234
150
+
151
+ # Histogram
152
+ orders_created_amount_bucket{currency="USD",le="100"} 456
153
+ orders_created_amount_bucket{currency="USD",le="500"} 1100
154
+ orders_created_amount_sum{currency="USD"} 298450.50
155
+ orders_created_amount_count{currency="USD"} 1234
156
+ ```
157
+
158
+ ---
159
+
160
+ ### Scenario 2: User Registration Funnel
161
+
162
+ **Funnel Events:**
163
+ 1. Registration Started
164
+ 2. Email Verified
165
+ 3. Profile Completed
166
+ 4. First Login
167
+
168
+ ```ruby
169
+ # Events
170
+ module Events
171
+ class RegistrationStarted < E11y::Event::Base
172
+ schema do
173
+ required(:user_id).filled(:string)
174
+ required(:source).filled(:string) # organic, referral, ad
175
+ end
176
+
177
+ severity :info
178
+ end
179
+
180
+ class EmailVerified < E11y::Event::Base
181
+ schema do
182
+ required(:user_id).filled(:string)
183
+ required(:verification_method).filled(:string) # email_link, code
184
+ end
185
+
186
+ severity :success
187
+ end
188
+
189
+ class ProfileCompleted < E11y::Event::Base
190
+ schema do
191
+ required(:user_id).filled(:string)
192
+ required(:fields_filled).array(:string)
193
+ end
194
+
195
+ severity :success
196
+ end
197
+
198
+ class FirstLogin < E11y::Event::Base
199
+ schema do
200
+ required(:user_id).filled(:string)
201
+ required(:time_since_registration_hours).filled(:integer)
202
+ end
203
+
204
+ severity :success
205
+ end
206
+ end
207
+
208
+ # Usage in controllers
209
+ class RegistrationsController < ApplicationController
210
+ def create
211
+ user = User.create!(registration_params)
212
+
213
+ Events::RegistrationStarted.track(
214
+ user_id: user.id,
215
+ source: params[:utm_source] || 'organic'
216
+ )
217
+
218
+ send_verification_email(user)
219
+ render json: user
220
+ end
221
+
222
+ def verify
223
+ user.verify_email!
224
+
225
+ Events::EmailVerified.track(
226
+ user_id: user.id,
227
+ verification_method: 'email_link'
228
+ )
229
+
230
+ redirect_to profile_path
231
+ end
232
+ end
233
+
234
+ # Metrics configuration
235
+ E11y.configure do |config|
236
+ config.metrics do
237
+ # Funnel counter
238
+ counter_for pattern: 'registration.*',
239
+ name: 'registration.funnel.total',
240
+ tags: [:event_name, :source]
241
+
242
+ # Time to first login
243
+ histogram_for pattern: 'first.login',
244
+ name: 'registration.time_to_first_login_hours',
245
+ value: ->(e) { e.payload[:time_since_registration_hours] },
246
+ buckets: [1, 6, 12, 24, 48, 72, 168] # hours
247
+ end
248
+ end
249
+ ```
250
+
251
+ **Funnel Analysis (Grafana/Prometheus):**
252
+ ```promql
253
+ # Conversion rate: Started → Verified
254
+ sum(registration_funnel_total{event_name="email.verified"}) /
255
+ sum(registration_funnel_total{event_name="registration.started"})
256
+ * 100
257
+
258
+ # Conversion rate: Verified → Completed
259
+ sum(registration_funnel_total{event_name="profile.completed"}) /
260
+ sum(registration_funnel_total{event_name="email.verified"})
261
+ * 100
262
+
263
+ # Median time to first login
264
+ histogram_quantile(0.5, rate(registration_time_to_first_login_hours_bucket[7d]))
265
+ ```
266
+
267
+ ---
268
+
269
+ ### Scenario 3: Payment Processing
270
+
271
+ **Events:**
272
+ 1. Payment Initiated
273
+ 2. Payment Processing
274
+ 3. Payment Succeeded / Failed
275
+
276
+ ```ruby
277
+ module Events
278
+ class PaymentInitiated < E11y::Event::Base
279
+ schema do
280
+ required(:payment_id).filled(:string)
281
+ required(:order_id).filled(:string)
282
+ required(:amount).filled(:decimal)
283
+ required(:currency).filled(:string)
284
+ required(:payment_method).filled(:string)
285
+ end
286
+
287
+ severity :info
288
+ end
289
+
290
+ class PaymentSucceeded < E11y::Event::Base
291
+ schema do
292
+ required(:payment_id).filled(:string)
293
+ required(:order_id).filled(:string)
294
+ required(:amount).filled(:decimal)
295
+ required(:currency).filled(:string)
296
+ required(:payment_method).filled(:string)
297
+ required(:processor_id).filled(:string) # Stripe charge ID
298
+ required(:duration_ms).filled(:integer)
299
+ end
300
+
301
+ severity :success # ← Key: success events easy to filter
302
+ end
303
+
304
+ class PaymentFailed < E11y::Event::Base
305
+ schema do
306
+ required(:payment_id).filled(:string)
307
+ required(:order_id).filled(:string)
308
+ required(:amount).filled(:decimal)
309
+ required(:error_code).filled(:string)
310
+ required(:error_message).filled(:string)
311
+ end
312
+
313
+ severity :error
314
+ end
315
+ end
316
+
317
+ # Usage
318
+ class ProcessPaymentJob < ApplicationJob
319
+ def perform(payment_id)
320
+ payment = Payment.find(payment_id)
321
+
322
+ Events::PaymentInitiated.track(
323
+ payment_id: payment.id,
324
+ order_id: payment.order_id,
325
+ amount: payment.amount,
326
+ currency: payment.currency,
327
+ payment_method: payment.method
328
+ )
329
+
330
+ # Track with duration measurement
331
+ Events::PaymentSucceeded.track(
332
+ payment_id: payment.id,
333
+ order_id: payment.order_id,
334
+ amount: payment.amount,
335
+ currency: payment.currency,
336
+ payment_method: payment.method,
337
+ processor_id: response.id
338
+ ) do
339
+ # Block execution time automatically measured
340
+ response = StripeClient.charge(payment.token, payment.amount)
341
+ payment.update!(status: 'succeeded', processor_id: response.id)
342
+ end
343
+
344
+ rescue Stripe::CardError => e
345
+ Events::PaymentFailed.track(
346
+ payment_id: payment.id,
347
+ order_id: payment.order_id,
348
+ amount: payment.amount,
349
+ error_code: e.code,
350
+ error_message: e.message
351
+ )
352
+ raise
353
+ end
354
+ end
355
+
356
+ # Metrics
357
+ E11y.configure do |config|
358
+ config.metrics do
359
+ # Success rate (critical metric!)
360
+ success_rate_for pattern: 'payment.*',
361
+ name: 'payments.success_rate',
362
+ tags: [:payment_method]
363
+ # Auto-calculates: succeeded / (succeeded + failed) * 100
364
+
365
+ # Payment duration (performance)
366
+ histogram_for pattern: 'payment.succeeded',
367
+ value: ->(e) { e.duration_ms },
368
+ name: 'payments.duration_ms',
369
+ tags: [:payment_method],
370
+ buckets: [100, 250, 500, 1000, 2000, 5000]
371
+
372
+ # Failed payments by error code (debugging)
373
+ counter_for pattern: 'payment.failed',
374
+ name: 'payments.failed.total',
375
+ tags: [:error_code, :payment_method]
376
+ end
377
+ end
378
+ ```
379
+
380
+ **Alerts (Prometheus):**
381
+ ```yaml
382
+ groups:
383
+ - name: payments
384
+ rules:
385
+ - alert: PaymentSuccessRateLow
386
+ expr: payments_success_rate{payment_method="stripe"} < 95
387
+ for: 5m
388
+ annotations:
389
+ summary: "Payment success rate below 95%"
390
+
391
+ - alert: PaymentHighLatency
392
+ expr: histogram_quantile(0.95, rate(payments_duration_ms_bucket[5m])) > 1000
393
+ annotations:
394
+ summary: "Payment p95 latency >1s"
395
+ ```
396
+
397
+ ---
398
+
399
+ ## 🔧 Configuration
400
+
401
+ ### Basic Event Definition
402
+
403
+ ```ruby
404
+ # app/events/user_logged_in.rb
405
+ module Events
406
+ class UserLoggedIn < E11y::Event::Base
407
+ # Schema definition with Dry::Schema
408
+ schema do
409
+ required(:user_id).filled(:string)
410
+ required(:ip_address).filled(:string, format?: /\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/)
411
+ optional(:user_agent).filled(:string)
412
+ end
413
+
414
+ # Optional: default severity
415
+ severity :info
416
+ end
417
+ end
418
+
419
+ # Usage
420
+ Events::UserLoggedIn.track(
421
+ user_id: 'user_123',
422
+ ip_address: '192.168.1.1',
423
+ user_agent: request.user_agent
424
+ )
425
+ ```
426
+
427
+ ---
428
+
429
+ ### Event-Level Configuration (NEW - v1.1)
430
+
431
+ > **🎯 CONTRADICTION_01 Resolution:** Move configuration from global initializer to event classes.
432
+ >
433
+ > **Goal:** Reduce global config from 1400+ lines to <300 lines by distributing settings across events.
434
+
435
+ **Event-level DSL:**
436
+
437
+ ```ruby
438
+ # app/events/order_created.rb
439
+ module Events
440
+ class OrderCreated < E11y::Event::Base
441
+ schema do
442
+ required(:order_id).filled(:string)
443
+ required(:amount).filled(:decimal)
444
+ required(:currency).filled(:string)
445
+ end
446
+
447
+ # ✨ Event-level configuration (right next to schema!)
448
+ severity :success
449
+ rate_limit 1000, window: 1.second # Max 1000 events/sec
450
+ sample_rate 0.1 # 10% sampling
451
+ retention 30.days # Keep for 30 days
452
+ adapters [:loki, :elasticsearch] # Override global adapters
453
+
454
+ # Metric definition
455
+ metric :counter,
456
+ name: 'orders.created.total',
457
+ tags: [:currency]
458
+ end
459
+ end
460
+ ```
461
+
462
+ **Available event-level settings:**
463
+
464
+ | Setting | Type | Example | Default |
465
+ |---------|------|---------|---------|
466
+ | `severity` | Symbol | `:success, :error, :debug` | Inferred from name |
467
+ | `rate_limit` | Integer + window | `1000, window: 1.second` | 1000/sec |
468
+ | `sample_rate` | Float | `0.1` (10%) | By severity |
469
+ | `retention` | Duration | `30.days, 7.years` | By severity |
470
+ | `adapters` | Array | `[:loki, :sentry]` | By severity |
471
+ | `adapters_strategy` | Symbol | `:replace, :append` | `:replace` |
472
+
473
+ **Precedence (event-level overrides global):**
474
+
475
+ ```ruby
476
+ # Global config (infrastructure):
477
+ E11y.configure do |config|
478
+ config.register_adapter :loki, Loki.new(url: ENV['LOKI_URL'])
479
+ config.default_adapters = [:loki]
480
+ config.default_sample_rate = 0.1 # 10%
481
+ end
482
+
483
+ # Event-level config (overrides global):
484
+ class Events::CriticalError < E11y::Event::Base
485
+ severity :fatal
486
+ sample_rate 1.0 # ← Override: 100% (not 10%)
487
+ adapters [:sentry, :pagerduty] # ← Override: not [:loki]
488
+ end
489
+ ```
490
+
491
+ ---
492
+
493
+ ### Event Inheritance (NEW - v1.1)
494
+
495
+ > **🎯 Pattern:** Use base classes to share common configuration across related events.
496
+
497
+ **Base class for payment events:**
498
+
499
+ ```ruby
500
+ # app/events/base_payment_event.rb
501
+ module Events
502
+ class BasePaymentEvent < E11y::Event::Base
503
+ # Common configuration for ALL payment events
504
+ severity :success
505
+ rate_limit 1000
506
+ sample_rate 1.0 # Never sample payments (high-value)
507
+ retention 7.years # Financial records
508
+ adapters [:loki, :sentry, :s3_archive]
509
+
510
+ # Common PII filtering
511
+ contains_pii true
512
+ pii_filtering do
513
+ hashes :email, :user_id # Pseudonymize
514
+ allows :order_id, :amount, :currency # Non-PII
515
+ end
516
+
517
+ # Common metric
518
+ metric :counter,
519
+ name: 'payments.total',
520
+ tags: [:currency, :payment_method]
521
+ end
522
+ end
523
+
524
+ # Inherit from base (1-2 lines per event!)
525
+ class Events::PaymentSucceeded < Events::BasePaymentEvent
526
+ schema do
527
+ required(:transaction_id).filled(:string)
528
+ required(:order_id).filled(:string)
529
+ required(:amount).filled(:decimal)
530
+ required(:currency).filled(:string)
531
+ required(:payment_method).filled(:string)
532
+ end
533
+ # ← Inherits ALL config from BasePaymentEvent!
534
+ end
535
+
536
+ class Events::PaymentFailed < Events::BasePaymentEvent
537
+ severity :error # ← Override base (errors, not success)
538
+
539
+ schema do
540
+ required(:transaction_id).filled(:string)
541
+ required(:order_id).filled(:string)
542
+ required(:amount).filled(:decimal)
543
+ required(:error_code).filled(:string)
544
+ required(:error_message).filled(:string)
545
+ end
546
+ # ← Inherits: rate_limit, sample_rate, retention, adapters, PII
547
+ end
548
+ ```
549
+
550
+ **Benefits:**
551
+ - ✅ 1-2 lines per event (just schema!)
552
+ - ✅ DRY (common config shared)
553
+ - ✅ Consistency (all payments have same config)
554
+ - ✅ Easy to change (update base → all events updated)
555
+
556
+ **More base class examples:**
557
+
558
+ ```ruby
559
+ # Base for audit events
560
+ module Events
561
+ class BaseAuditEvent < E11y::Event::Base
562
+ severity :warn
563
+ audit_event true
564
+ adapters [:audit_encrypted]
565
+ # ← Auto-set by audit_event:
566
+ # retention = E11y.config.audit_retention (default: 7.years, configurable!)
567
+ # rate_limiting = false (LOCKED!)
568
+ # sampling = false (LOCKED!)
569
+
570
+ signing do
571
+ enabled true
572
+ algorithm :ed25519
573
+ end
574
+ end
575
+ end
576
+
577
+ # Base for debug events
578
+ module Events
579
+ class BaseDebugEvent < E11y::Event::Base
580
+ severity :debug
581
+ rate_limit 100
582
+ sample_rate 0.01 # 1%
583
+ retention 7.days
584
+ adapters [:file] # Local only
585
+ contains_pii false
586
+ end
587
+ end
588
+ ```
589
+
590
+ ---
591
+
592
+ ### Preset Modules (NEW - v1.1)
593
+
594
+ > **🎯 Pattern:** Use preset modules for 1-line configuration includes (Rails-style concerns).
595
+
596
+ **Built-in presets:**
597
+
598
+ ```ruby
599
+ # E11y provides built-in presets:
600
+ module E11y
601
+ module Presets
602
+ module HighValueEvent
603
+ extend ActiveSupport::Concern
604
+ included do
605
+ rate_limit 10_000
606
+ sample_rate 1.0 # Never sample
607
+ retention 7.years
608
+ adapters [:loki, :sentry, :s3_archive]
609
+ end
610
+ end
611
+
612
+ module DebugEvent
613
+ extend ActiveSupport::Concern
614
+ included do
615
+ severity :debug
616
+ rate_limit 100
617
+ sample_rate 0.01
618
+ retention 7.days
619
+ adapters [:file]
620
+ end
621
+ end
622
+
623
+ module AuditEvent
624
+ extend ActiveSupport::Concern
625
+ included do
626
+ audit_event true
627
+ adapters [:audit_encrypted]
628
+ retention 7.years
629
+ rate_limiting false
630
+ sampling false
631
+ end
632
+ end
633
+ end
634
+ end
635
+ ```
636
+
637
+ **Usage (1-line includes!):**
638
+
639
+ ```ruby
640
+ class Events::PaymentProcessed < E11y::Event::Base
641
+ include E11y::Presets::HighValueEvent # ← All config inherited!
642
+
643
+ schema do
644
+ required(:transaction_id).filled(:string)
645
+ required(:amount).filled(:decimal)
646
+ end
647
+ end
648
+
649
+ class Events::DebugSqlQuery < E11y::Event::Base
650
+ include E11y::Presets::DebugEvent # ← All config inherited!
651
+
652
+ schema do
653
+ required(:query).filled(:string)
654
+ required(:duration_ms).filled(:float)
655
+ end
656
+ end
657
+ ```
658
+
659
+ **Custom presets (project-specific):**
660
+
661
+ ```ruby
662
+ # app/events/presets/critical_business_event.rb
663
+ module Events
664
+ module Presets
665
+ module CriticalBusinessEvent
666
+ extend ActiveSupport::Concern
667
+ included do
668
+ severity :success
669
+ rate_limit 5000
670
+ sample_rate 1.0
671
+ retention 7.years
672
+ adapters [:loki, :elasticsearch, :s3_archive, :slack_business]
673
+
674
+ metric :counter,
675
+ name: 'critical_business_events.total',
676
+ tags: [:event_name]
677
+ end
678
+ end
679
+ end
680
+ end
681
+
682
+ # Usage:
683
+ class Events::LargeOrderPlaced < E11y::Event::Base
684
+ include Events::Presets::CriticalBusinessEvent
685
+
686
+ schema do
687
+ required(:order_id).filled(:string)
688
+ required(:amount).filled(:decimal)
689
+ end
690
+ end
691
+ ```
692
+
693
+ ---
694
+
695
+ ### Conventions & Sensible Defaults (NEW - v1.1)
696
+
697
+ > **Philosophy:** "Explicit over implicit" + conventions = best balance.
698
+ >
699
+ > E11y applies **sensible defaults** to eliminate 80% of configuration.
700
+
701
+ **Convention 1: Event Name → Severity**
702
+
703
+ ```ruby
704
+ # *Failed, *Error → :error
705
+ class Events::PaymentFailed < E11y::Event::Base
706
+ # ← Auto: severity = :error
707
+ end
708
+
709
+ # *Paid, *Succeeded, *Completed → :success
710
+ class Events::OrderPaid < E11y::Event::Base
711
+ # ← Auto: severity = :success
712
+ end
713
+
714
+ # *Started, *Processing → :info
715
+ class Events::OrderProcessing < E11y::Event::Base
716
+ # ← Auto: severity = :info
717
+ end
718
+
719
+ # Debug* → :debug
720
+ class Events::DebugQuery < E11y::Event::Base
721
+ # ← Auto: severity = :debug
722
+ end
723
+ ```
724
+
725
+ **Convention 2: Severity → Adapters**
726
+
727
+ ```ruby
728
+ # :error/:fatal → [:sentry]
729
+ # :success/:info/:warn → [:loki]
730
+ # :debug → [:file] (dev), [:loki] (prod)
731
+ ```
732
+
733
+ **Convention 3: Severity → Sample Rate**
734
+
735
+ ```ruby
736
+ # :error/:fatal → 1.0 (100%)
737
+ # :warn → 0.5 (50%)
738
+ # :success/:info → 0.1 (10%)
739
+ # :debug → 0.01 (1%)
740
+ ```
741
+
742
+ **Convention 4: Severity → Retention**
743
+
744
+ ```ruby
745
+ # :error/:fatal → 90 days
746
+ # :info/:success → 30 days
747
+ # :debug → 7 days
748
+ ```
749
+
750
+ **Result: Zero-Config Events**
751
+
752
+ ```ruby
753
+ # 90% of events need ONLY schema!
754
+ class Events::OrderCreated < E11y::Event::Base
755
+ schema do
756
+ required(:order_id).filled(:string)
757
+ required(:amount).filled(:decimal)
758
+ end
759
+ # ← That's it! All config from conventions:
760
+ # severity: :success (from name)
761
+ # adapters: [:loki] (from severity)
762
+ # sample_rate: 0.1 (from severity)
763
+ # retention: 30.days (from severity)
764
+ # rate_limit: 1000 (default)
765
+ end
766
+ ```
767
+
768
+ **Override when needed:**
769
+
770
+ ```ruby
771
+ class Events::OrderCreated < E11y::Event::Base
772
+ schema do; required(:order_id).filled(:string); end
773
+
774
+ severity :info # ← Override convention
775
+ sample_rate 1.0 # ← Never sample
776
+ retention 7.years # ← Financial records
777
+ end
778
+ ```
779
+
780
+ ### Event Naming Conventions
781
+
782
+ **Recommended pattern:** `<entity>.<past_tense_verb>`
783
+
784
+ ```ruby
785
+ # ✅ GOOD naming
786
+ Events::OrderCreated # order.created
787
+ Events::OrderPaid # order.paid
788
+ Events::OrderShipped # order.shipped
789
+ Events::UserRegistered # user.registered
790
+ Events::PaymentProcessed # payment.processed
791
+
792
+ # ❌ BAD naming
793
+ Events::CreateOrder # Present tense (not an event!)
794
+ Events::OrderCreate # Wrong order
795
+ Events::Order # Too generic
796
+ Events::OrderEvent # Redundant suffix
797
+ ```
798
+
799
+ ---
800
+
801
+ ## 🔧 Adapter Routing (Per-Event)
802
+
803
+ ### Override Adapters for Specific Events
804
+
805
+ **Step 1: Define adapters in global config (one place!):**
806
+ ```ruby
807
+ # config/initializers/e11y.rb
808
+ E11y.configure do |config|
809
+ # Register named adapters (created once with connections)
810
+ config.register_adapter :loki, E11y::Adapters::LokiAdapter.new(
811
+ url: ENV['LOKI_URL']
812
+ )
813
+
814
+ config.register_adapter :file, E11y::Adapters::FileAdapter.new(
815
+ path: 'log/e11y'
816
+ )
817
+
818
+ config.register_adapter :sentry, E11y::Adapters::SentryAdapter.new(
819
+ dsn: ENV['SENTRY_DSN'],
820
+ environment: Rails.env
821
+ )
822
+
823
+ config.register_adapter :pagerduty, E11y::Adapters::PagerDutyAdapter.new(
824
+ api_key: ENV['PAGERDUTY_KEY'],
825
+ service_id: ENV['PAGERDUTY_SERVICE_ID']
826
+ )
827
+
828
+ config.register_adapter :slack, E11y::Adapters::SlackAdapter.new(
829
+ webhook_url: ENV['SLACK_WEBHOOK_URL'],
830
+ channel: '#alerts'
831
+ )
832
+
833
+ # Default adapters (used by all events unless overridden)
834
+ config.default_adapters = [:loki, :file]
835
+ end
836
+ ```
837
+
838
+ **Step 2: Reference adapters by name in events:**
839
+ ```ruby
840
+ # app/events/critical_error.rb
841
+ module Events
842
+ class CriticalError < E11y::Event::Base
843
+ severity :fatal
844
+
845
+ schema do
846
+ required(:error).filled(:string)
847
+ required(:context).filled(:hash)
848
+ end
849
+
850
+ # Override: Send ONLY to Sentry (reference by name!)
851
+ adapters [:sentry]
852
+ end
853
+ end
854
+
855
+ # Usage
856
+ Events::CriticalError.track(
857
+ error: 'Database connection lost',
858
+ context: { db_host: 'prod-db-1' }
859
+ )
860
+ # → Sent ONLY to :sentry adapter ✅
861
+ # → NOT sent to :loki or :file ✅
862
+ ```
863
+
864
+ ### Use Cases for Adapter Override
865
+
866
+ **1. Security Events → Separate Audit Log**
867
+ ```ruby
868
+ # config/initializers/e11y.rb
869
+ E11y.configure do |config|
870
+ # Register security audit adapter
871
+ config.register_adapter :security_audit, E11y::Adapters::FileAdapter.new(
872
+ path: 'log/security_audit',
873
+ permissions: 0600 # Restricted access
874
+ )
875
+
876
+ # Other adapters...
877
+ config.register_adapter :loki, E11y::Adapters::LokiAdapter.new(...)
878
+ config.default_adapters = [:loki]
879
+ end
880
+
881
+ # app/events/security_audit_event.rb
882
+ module Events
883
+ class SecurityAuditEvent < E11y::Event::Base
884
+ severity :warn
885
+
886
+ # Route to secure audit log ONLY (reference by name!)
887
+ adapters [:security_audit]
888
+ end
889
+
890
+ class UserPermissionChanged < SecurityAuditEvent
891
+ schema do
892
+ required(:user_id).filled(:string)
893
+ required(:old_role).filled(:string)
894
+ required(:new_role).filled(:string)
895
+ required(:changed_by).filled(:string)
896
+ end
897
+ end
898
+ end
899
+
900
+ # Goes to :security_audit adapter ONLY
901
+ Events::UserPermissionChanged.track(
902
+ user_id: 'user_123',
903
+ old_role: 'user',
904
+ new_role: 'admin',
905
+ changed_by: 'admin_456'
906
+ )
907
+ ```
908
+
909
+ **2. High-Volume Debug Events → Local File Only**
910
+ ```ruby
911
+ # config/initializers/e11y.rb
912
+ E11y.configure do |config|
913
+ # Register debug file adapter
914
+ config.register_adapter :debug_file, E11y::Adapters::FileAdapter.new(
915
+ path: 'log/sql_queries',
916
+ rotation: :daily
917
+ )
918
+
919
+ config.register_adapter :loki, E11y::Adapters::LokiAdapter.new(...)
920
+ config.default_adapters = [:loki] # Default: Loki for all
921
+ end
922
+
923
+ # app/events/debug_sql_query.rb
924
+ module Events
925
+ class DebugSqlQuery < E11y::Event::Base
926
+ severity :debug
927
+
928
+ # Don't send to Loki (too expensive!)
929
+ # Write to local file only (reference by name!)
930
+ adapters [:debug_file]
931
+
932
+ schema do
933
+ required(:query).filled(:string)
934
+ required(:duration_ms).filled(:float)
935
+ end
936
+ end
937
+ end
938
+
939
+ # High-volume events don't flood Loki
940
+ 1000.times do
941
+ Events::DebugSqlQuery.track(query: 'SELECT ...', duration_ms: 1.2)
942
+ end
943
+ # → All written to :debug_file adapter ✅
944
+ # → Loki bills stay low ✅
945
+ ```
946
+
947
+ **3. Critical Alerts → Multiple Destinations**
948
+ ```ruby
949
+ # config/initializers/e11y.rb
950
+ E11y.configure do |config|
951
+ # Register all adapters once
952
+ config.register_adapter :sentry, E11y::Adapters::SentryAdapter.new(
953
+ dsn: ENV['SENTRY_DSN']
954
+ )
955
+
956
+ config.register_adapter :pagerduty, E11y::Adapters::PagerDutyAdapter.new(
957
+ api_key: ENV['PAGERDUTY_KEY'],
958
+ service_id: ENV['PAGERDUTY_SERVICE_ID']
959
+ )
960
+
961
+ config.register_adapter :slack_fraud, E11y::Adapters::SlackAdapter.new(
962
+ webhook_url: ENV['SLACK_WEBHOOK_URL'],
963
+ channel: '#fraud-alerts'
964
+ )
965
+
966
+ config.register_adapter :fraud_audit, E11y::Adapters::FileAdapter.new(
967
+ path: 'log/fraud_audit',
968
+ permissions: 0600
969
+ )
970
+
971
+ config.register_adapter :loki, E11y::Adapters::LokiAdapter.new(...)
972
+ config.default_adapters = [:loki]
973
+ end
974
+
975
+ # app/events/payment_fraud_detected.rb
976
+ module Events
977
+ class PaymentFraudDetected < E11y::Event::Base
978
+ severity :fatal
979
+
980
+ # Send to multiple destinations! (reference by name)
981
+ adapters [:sentry, :pagerduty, :slack_fraud, :fraud_audit]
982
+
983
+ schema do
984
+ required(:transaction_id).filled(:string)
985
+ required(:user_id).filled(:string)
986
+ required(:fraud_score).filled(:float)
987
+ required(:reasons).array(:string)
988
+ end
989
+ end
990
+ end
991
+
992
+ # One event → 4 destinations!
993
+ Events::PaymentFraudDetected.track(
994
+ transaction_id: 'tx_123',
995
+ user_id: 'user_456',
996
+ fraud_score: 0.95,
997
+ reasons: ['velocity_check_failed', 'suspicious_location']
998
+ )
999
+ # → :sentry alert ✅
1000
+ # → :pagerduty incident ✅
1001
+ # → :slack_fraud notification ✅
1002
+ # → :fraud_audit log written ✅
1003
+ ```
1004
+
1005
+ **4. Inherit + Extend Global Adapters**
1006
+ ```ruby
1007
+ # config/initializers/e11y.rb
1008
+ E11y.configure do |config|
1009
+ config.register_adapter :loki, E11y::Adapters::LokiAdapter.new(...)
1010
+ config.register_adapter :file, E11y::Adapters::FileAdapter.new(...)
1011
+ config.register_adapter :slack_business, E11y::Adapters::SlackAdapter.new(
1012
+ webhook_url: ENV['SLACK_WEBHOOK_URL'],
1013
+ channel: '#business-events'
1014
+ )
1015
+
1016
+ config.default_adapters = [:loki, :file]
1017
+ end
1018
+
1019
+ # app/events/important_business_event.rb
1020
+ module Events
1021
+ class ImportantBusinessEvent < E11y::Event::Base
1022
+ # Strategy: add to default adapters (not replace)
1023
+ adapters_strategy :append # :append or :replace (default)
1024
+
1025
+ # Add Slack to global adapters
1026
+ adapters [:slack_business]
1027
+ end
1028
+
1029
+ class LargeOrderPlaced < ImportantBusinessEvent
1030
+ schema do
1031
+ required(:order_id).filled(:string)
1032
+ required(:amount).filled(:decimal)
1033
+ end
1034
+ end
1035
+ end
1036
+
1037
+ # Goes to: :loki (global) + :file (global) + :slack_business (added) ✅
1038
+ Events::LargeOrderPlaced.track(
1039
+ order_id: 'ord_123',
1040
+ amount: 10000.00
1041
+ )
1042
+ ```
1043
+
1044
+ **5. Environment-Specific Routing**
1045
+ ```ruby
1046
+ # config/initializers/e11y.rb
1047
+ E11y.configure do |config|
1048
+ # Register adapters per environment
1049
+ case Rails.env
1050
+ when 'production'
1051
+ config.register_adapter :loki, E11y::Adapters::LokiAdapter.new(
1052
+ url: ENV['LOKI_URL']
1053
+ )
1054
+ config.register_adapter :s3_archive, E11y::Adapters::S3Adapter.new(
1055
+ bucket: 'payment-archive'
1056
+ )
1057
+ config.default_adapters = [:loki]
1058
+
1059
+ when 'staging'
1060
+ config.register_adapter :loki, E11y::Adapters::LokiAdapter.new(
1061
+ url: ENV['STAGING_LOKI_URL']
1062
+ )
1063
+ config.default_adapters = [:loki]
1064
+
1065
+ when 'development'
1066
+ config.register_adapter :console, E11y::Adapters::ConsoleAdapter.new(
1067
+ colored: true
1068
+ )
1069
+ config.default_adapters = [:console]
1070
+
1071
+ when 'test'
1072
+ config.register_adapter :memory, E11y::Adapters::MemoryAdapter.new
1073
+ config.default_adapters = [:memory]
1074
+ end
1075
+ end
1076
+
1077
+ # app/events/payment_processed.rb
1078
+ module Events
1079
+ class PaymentProcessed < E11y::Event::Base
1080
+ schema do
1081
+ required(:transaction_id).filled(:string)
1082
+ required(:amount).filled(:decimal)
1083
+ end
1084
+
1085
+ # Production: also archive to S3
1086
+ if Rails.env.production?
1087
+ adapters [:loki, :s3_archive]
1088
+ end
1089
+ # Other envs: use default_adapters
1090
+ end
1091
+ end
1092
+ ```
1093
+
1094
+ ---
1095
+
1096
+ ## 📊 Metrics Configuration
1097
+
1098
+ ### Pattern-Based Auto-Metrics
1099
+
1100
+ ```ruby
1101
+ E11y.configure do |config|
1102
+ config.metrics do
1103
+ # Global counter for ALL events
1104
+ counter_for pattern: '*',
1105
+ name: 'business_events.total',
1106
+ tags: [:event_name, :severity]
1107
+
1108
+ # Domain-specific counters
1109
+ counter_for pattern: 'order.*',
1110
+ name: 'orders.events.total',
1111
+ tags: [:event_name]
1112
+
1113
+ counter_for pattern: 'user.*',
1114
+ name: 'users.events.total',
1115
+ tags: [:event_name]
1116
+
1117
+ # Histograms for amounts/durations
1118
+ histogram_for pattern: '*.paid',
1119
+ name: 'payments.amount',
1120
+ value: ->(e) { e.payload[:amount] },
1121
+ tags: [:currency],
1122
+ buckets: [10, 50, 100, 500, 1000, 5000, 10000]
1123
+
1124
+ # Success rate (special metric type)
1125
+ success_rate_for pattern: 'payment.*',
1126
+ name: 'payments.success_rate'
1127
+ # Automatically calculates from :success and :error events
1128
+ end
1129
+ end
1130
+ ```
1131
+
1132
+ ---
1133
+
1134
+ ## ⚙️ Advanced: Custom Middleware
1135
+
1136
+ > **Implementation:** See [ADR-001 Section 7: Extension Points](../ADR-001-architecture.md#7-extension-points) for detailed architecture.
1137
+
1138
+ E11y allows you to extend the event processing pipeline with custom middleware. This is useful for:
1139
+ - Adding custom enrichment logic
1140
+ - Implementing custom filtering/transformation
1141
+ - Integrating with third-party services
1142
+ - Adding business-specific validation
1143
+
1144
+ ### Custom Middleware Example
1145
+
1146
+ **Step 1: Define Custom Middleware**
1147
+
1148
+ ```ruby
1149
+ # lib/e11y/middleware/priority_enrichment.rb
1150
+ module E11y
1151
+ module Middleware
1152
+ class PriorityEnrichment < E11y::Middleware
1153
+ def call(event_data)
1154
+ # Add priority field based on business logic
1155
+ if event_data[:payload][:user_role] == 'admin'
1156
+ event_data[:payload][:priority] = 'high'
1157
+ elsif event_data[:payload][:amount].to_f > 10_000
1158
+ event_data[:payload][:priority] = 'high'
1159
+ else
1160
+ event_data[:payload][:priority] = 'normal'
1161
+ end
1162
+
1163
+ # Continue pipeline
1164
+ @app.call(event_data)
1165
+ end
1166
+ end
1167
+ end
1168
+ end
1169
+ ```
1170
+
1171
+ **Step 2: Register Middleware**
1172
+
1173
+ ```ruby
1174
+ # config/initializers/e11y.rb
1175
+ E11y.configure do |config|
1176
+ # Register in correct order (see UC-001 for order requirements)
1177
+ config.pipeline.use E11y::Middleware::TraceContext
1178
+ config.pipeline.use E11y::Middleware::Validation
1179
+ config.pipeline.use E11y::Middleware::PiiFilter
1180
+ config.pipeline.use E11y::Middleware::PriorityEnrichment # ← Custom middleware
1181
+ config.pipeline.use E11y::Middleware::Routing
1182
+ end
1183
+ ```
1184
+
1185
+ **Step 3: Use Enriched Data**
1186
+
1187
+ ```ruby
1188
+ # Events now have priority field
1189
+ Events::OrderPaid.track(
1190
+ order_id: 'ORD-123',
1191
+ amount: 15_000.00,
1192
+ user_role: 'admin'
1193
+ )
1194
+
1195
+ # Result:
1196
+ # {
1197
+ # event_name: "order.paid",
1198
+ # payload: {
1199
+ # order_id: "ORD-123",
1200
+ # amount: 15000.0,
1201
+ # user_role: "admin",
1202
+ # priority: "high" # ← Automatically added by middleware!
1203
+ # }
1204
+ # }
1205
+ ```
1206
+
1207
+ ### Use Cases for Custom Middleware
1208
+
1209
+ **1. Tenant/Organization Isolation**
1210
+
1211
+ ```ruby
1212
+ class TenantMiddleware < E11y::Middleware
1213
+ def call(event_data)
1214
+ # Add tenant_id from current context
1215
+ event_data[:payload][:tenant_id] = Current.tenant_id
1216
+ event_data[:tenant_id] = Current.tenant_id # Top-level for filtering
1217
+
1218
+ @app.call(event_data)
1219
+ end
1220
+ end
1221
+
1222
+ # Now all events automatically tagged with tenant
1223
+ Events::OrderCreated.track(order_id: 'ORD-123')
1224
+ # → { event_name: "order.created", tenant_id: "tenant_456", payload: {...} }
1225
+ ```
1226
+
1227
+ **2. A/B Test Tracking**
1228
+
1229
+ ```ruby
1230
+ class ExperimentMiddleware < E11y::Middleware
1231
+ def call(event_data)
1232
+ # Add experiment variant from session
1233
+ if Current.user && Current.user.experiment_variants.present?
1234
+ event_data[:payload][:experiments] = Current.user.experiment_variants
1235
+ end
1236
+
1237
+ @app.call(event_data)
1238
+ end
1239
+ end
1240
+
1241
+ # Events now include A/B test info
1242
+ Events::CheckoutCompleted.track(order_id: 'ORD-123')
1243
+ # → { payload: { ..., experiments: { checkout_flow: "variant_b" } } }
1244
+ ```
1245
+
1246
+ **3. Custom Rate Limiting**
1247
+
1248
+ ```ruby
1249
+ class CustomRateLimiter < E11y::Middleware
1250
+ def initialize(app)
1251
+ super
1252
+ @limiter = RateLimiter.new
1253
+ end
1254
+
1255
+ def call(event_data)
1256
+ key = "#{event_data[:event_name]}:#{event_data[:payload][:user_id]}"
1257
+
1258
+ if @limiter.exceeded?(key, limit: 100, period: 60)
1259
+ # Drop event (don't call @app.call)
1260
+ E11y.logger.warn("Rate limit exceeded for #{key}")
1261
+ return :rate_limited
1262
+ end
1263
+
1264
+ @limiter.increment(key)
1265
+ @app.call(event_data)
1266
+ end
1267
+ end
1268
+ ```
1269
+
1270
+ **4. Conditional Adapter Routing**
1271
+
1272
+ ```ruby
1273
+ class DynamicRoutingMiddleware < E11y::Middleware
1274
+ def call(event_data)
1275
+ # Route high-priority events to PagerDuty
1276
+ if event_data[:payload][:priority] == 'critical'
1277
+ event_data[:adapters] ||= []
1278
+ event_data[:adapters] << :pagerduty
1279
+ end
1280
+
1281
+ @app.call(event_data)
1282
+ end
1283
+ end
1284
+ ```
1285
+
1286
+ ### Middleware Order Matters!
1287
+
1288
+ > ⚠️ **CRITICAL:** Middleware order determines the sequence of processing. See [UC-001 Configuration](./UC-001-request-scoped-debug-buffering.md#-configuration) for detailed explanation of middleware order requirements.
1289
+
1290
+ **General Order Rules:**
1291
+ 1. **Enrichment** (trace context, tenant_id) → FIRST
1292
+ 2. **Validation** (schema checks) → EARLY (fail fast)
1293
+ 3. **Security** (PII filtering) → BEFORE business logic
1294
+ 4. **Business Logic** (custom enrichment, rate limiting) → MIDDLE
1295
+ 5. **Routing** (buffer/adapter selection) → LAST
1296
+
1297
+ **Example Correct Order:**
1298
+
1299
+ ```ruby
1300
+ E11y.configure do |config|
1301
+ # 1. Enrichment
1302
+ config.pipeline.use E11y::Middleware::TraceContext
1303
+ config.pipeline.use TenantMiddleware
1304
+
1305
+ # 2. Validation
1306
+ config.pipeline.use E11y::Middleware::Validation
1307
+
1308
+ # 3. Security
1309
+ config.pipeline.use E11y::Middleware::PiiFilter
1310
+
1311
+ # 4. Business Logic
1312
+ config.pipeline.use PriorityEnrichment
1313
+ config.pipeline.use ExperimentMiddleware
1314
+ config.pipeline.use CustomRateLimiter
1315
+
1316
+ # 5. Routing (LAST!)
1317
+ config.pipeline.use E11y::Middleware::Routing
1318
+ end
1319
+ ```
1320
+
1321
+ ---
1322
+
1323
+ ## 🧪 Testing
1324
+
1325
+ ### Unit Test Event Class
1326
+
1327
+ ```ruby
1328
+ # spec/events/order_created_spec.rb
1329
+ RSpec.describe Events::OrderCreated do
1330
+ it 'validates required attributes' do
1331
+ expect {
1332
+ described_class.track(
1333
+ order_id: nil, # Invalid!
1334
+ user_id: 'user_123',
1335
+ total_amount: 99.99
1336
+ )
1337
+ }.to raise_error(E11y::ValidationError)
1338
+ end
1339
+
1340
+ it 'validates amount is positive' do
1341
+ expect {
1342
+ described_class.track(
1343
+ order_id: 'ORD-123',
1344
+ user_id: 'user_123',
1345
+ total_amount: -10 # Invalid!
1346
+ )
1347
+ }.to raise_error(E11y::ValidationError, /must be greater than/)
1348
+ end
1349
+
1350
+ it 'tracks valid event' do
1351
+ expect(E11y::Collector).to receive(:collect).with(
1352
+ have_attributes(
1353
+ name: 'order.created',
1354
+ severity: :success,
1355
+ payload: hash_including(order_id: 'ORD-123')
1356
+ )
1357
+ )
1358
+
1359
+ described_class.track(
1360
+ order_id: 'ORD-123',
1361
+ user_id: 'user_123',
1362
+ items_count: 3,
1363
+ total_amount: 99.99,
1364
+ currency: 'USD'
1365
+ )
1366
+ end
1367
+ end
1368
+ ```
1369
+
1370
+ ### Integration Test Controller
1371
+
1372
+ ```ruby
1373
+ # spec/controllers/orders_controller_spec.rb
1374
+ RSpec.describe OrdersController, type: :controller do
1375
+ it 'tracks order creation event' do
1376
+ expect(Events::OrderCreated).to receive(:track).with(
1377
+ hash_including(
1378
+ order_id: anything,
1379
+ user_id: current_user.id
1380
+ )
1381
+ )
1382
+
1383
+ post :create, params: { sku: 'ABC123', quantity: 1 }
1384
+
1385
+ expect(response).to be_successful
1386
+ end
1387
+ end
1388
+ ```
1389
+
1390
+ ---
1391
+
1392
+ ## 💡 Best Practices
1393
+
1394
+ ### ✅ DO
1395
+
1396
+ 1. **Use past tense for event names**
1397
+ ```ruby
1398
+ Events::OrderCreated # ✅
1399
+ Events::CreateOrder # ❌
1400
+ ```
1401
+
1402
+ 2. **Include business-meaningful attributes**
1403
+ ```ruby
1404
+ # ✅ Good: can answer business questions
1405
+ Events::OrderPaid.track(
1406
+ order_id: order.id,
1407
+ amount: order.total,
1408
+ currency: order.currency,
1409
+ payment_method: 'stripe',
1410
+ user_segment: user.segment # NEW, HIGH, RETURNING
1411
+ )
1412
+
1413
+ # ❌ Bad: only technical details
1414
+ Events::OrderPaid.track(order_id: order.id)
1415
+ ```
1416
+
1417
+ 3. **Use :success severity for completed operations**
1418
+ ```ruby
1419
+ Events::OrderPaid.track(..., severity: :success) # ✅
1420
+ Events::OrderPaid.track(..., severity: :info) # ❌ Harder to filter
1421
+ ```
1422
+
1423
+ 4. **Measure duration for long-running operations**
1424
+ ```ruby
1425
+ Events::PaymentProcessed.track(...) do
1426
+ # Duration automatically measured
1427
+ process_payment
1428
+ end
1429
+ ```
1430
+
1431
+ 5. **Override adapters for special event types**
1432
+ ```ruby
1433
+ # ✅ Good: Critical events to multiple destinations (reference by name!)
1434
+ class CriticalError < E11y::Event::Base
1435
+ adapters [:sentry, :pagerduty, :slack]
1436
+ end
1437
+
1438
+ # ✅ Good: High-volume debug to local file only
1439
+ class DebugEvent < E11y::Event::Base
1440
+ adapters [:debug_file]
1441
+ end
1442
+ ```
1443
+
1444
+ ### ❌ DON'T
1445
+
1446
+ 1. **Don't log technical details as business events**
1447
+ ```ruby
1448
+ # ❌ Technical, not business event
1449
+ Events::DatabaseQuery.track(sql: '...', severity: :debug)
1450
+
1451
+ # ✅ Use :debug severity and request-scoped buffering
1452
+ Events::DatabaseQuery.track(sql: '...', severity: :debug)
1453
+ ```
1454
+
1455
+ 2. **Don't include PII in event names/attributes without filtering**
1456
+ ```ruby
1457
+ # ❌ PII leak!
1458
+ Events::UserRegistered.track(
1459
+ email: 'user@example.com', # ← Will be filtered if configured
1460
+ password: 'secret123' # ← NEVER include passwords!
1461
+ )
1462
+
1463
+ # ✅ PII filtered by Rails config
1464
+ # config/application.rb
1465
+ config.filter_parameters += [:email, :password]
1466
+ ```
1467
+
1468
+ 3. **Don't create too many event types**
1469
+ ```ruby
1470
+ # ❌ Over-engineering
1471
+ Events::OrderCreatedInProduction
1472
+ Events::OrderCreatedInStaging
1473
+ Events::OrderCreatedInDev
1474
+
1475
+ # ✅ Use context enrichment
1476
+ Events::OrderCreated # context[:env] auto-added
1477
+ ```
1478
+
1479
+ 4. **Don't override adapters for every event**
1480
+ ```ruby
1481
+ # ❌ Bad: Repetitive adapter references
1482
+ class OrderCreated < E11y::Event::Base
1483
+ adapters [:loki] # Same as default!
1484
+ end
1485
+
1486
+ class OrderPaid < E11y::Event::Base
1487
+ adapters [:loki] # Duplication!
1488
+ end
1489
+
1490
+ # ✅ Good: Use default_adapters, override only when needed
1491
+ # config/initializers/e11y.rb
1492
+ config.default_adapters = [:loki]
1493
+
1494
+ # Most events just use defaults (no adapters line needed!)
1495
+ class OrderCreated < E11y::Event::Base
1496
+ # Uses default_adapters automatically ✅
1497
+ end
1498
+
1499
+ # Override only for special cases
1500
+ class CriticalError < E11y::Event::Base
1501
+ adapters [:sentry] # Different from default!
1502
+ end
1503
+ ```
1504
+
1505
+ 5. **Don't create adapter instances in event classes**
1506
+ ```ruby
1507
+ # ❌ Bad: Creating adapter instances (defeats the purpose!)
1508
+ class MyEvent < E11y::Event::Base
1509
+ adapters [
1510
+ E11y::Adapters::LokiAdapter.new(url: ...) # ← NO!
1511
+ ]
1512
+ end
1513
+
1514
+ # ✅ Good: Reference by name (adapters created once in config)
1515
+ class MyEvent < E11y::Event::Base
1516
+ adapters [:loki] # ← YES!
1517
+ end
1518
+ ```
1519
+
1520
+ ---
1521
+
1522
+ ## ⚡ Performance Guarantees
1523
+
1524
+ > **Implementation:** See [ADR-001 Section 8: Performance Requirements](../ADR-001-architecture.md#8-performance-requirements) for detailed architecture targets.
1525
+
1526
+ E11y is designed for **high-performance production environments** with strict SLAs:
1527
+
1528
+ ### Service Level Objectives (SLOs)
1529
+
1530
+ | Metric | Target | Critical? |
1531
+ |--------|--------|-----------|
1532
+ | **Event Track Latency (p99)** | <1ms | ✅ Critical |
1533
+ | **Memory Footprint @ Steady State** | <100MB | ✅ Critical |
1534
+ | **Sustained Throughput** | 1000 events/sec | ✅ Critical |
1535
+ | **Burst Throughput** | 5000 events/sec (5s) | ⚠️ Important |
1536
+ | **CPU Usage @ 1000 evt/s** | <5% | ⚠️ Important |
1537
+
1538
+ ### What This Means for Your Application
1539
+
1540
+ **1. Track() Calls are Near-Zero Overhead**
1541
+
1542
+ ```ruby
1543
+ # Benchmark: 1000 events/sec
1544
+ Benchmark.ips do |x|
1545
+ x.report("E11y.track") do
1546
+ Events::OrderPaid.track(
1547
+ order_id: 'ORD-123',
1548
+ amount: 99.99
1549
+ )
1550
+ end
1551
+ end
1552
+
1553
+ # Results:
1554
+ # E11y.track: 100,000 i/s → ~0.01ms per call
1555
+ # p99 latency: <1ms ✅
1556
+ ```
1557
+
1558
+ **2. Memory-Efficient (No Memory Leaks)**
1559
+
1560
+ ```ruby
1561
+ # Memory profile @ 1000 events/sec for 1 hour
1562
+ # - Before E11y: 200MB RSS
1563
+ # - After E11y: 290MB RSS (+90MB)
1564
+ # - E11y footprint: ~90MB (within <100MB target ✅)
1565
+
1566
+ # No memory growth over time:
1567
+ # Hour 1: 290MB
1568
+ # Hour 24: 291MB (stable)
1569
+ # Week 1: 290MB (no leaks ✅)
1570
+ ```
1571
+
1572
+ **3. Non-Blocking Architecture**
1573
+
1574
+ ```ruby
1575
+ # track() is async - doesn't block request thread
1576
+ def create_order
1577
+ order = Order.create!(params)
1578
+
1579
+ # This call returns immediately (<1ms)
1580
+ Events::OrderCreated.track(order_id: order.id)
1581
+ # ↑ Event buffered, flushed in background
1582
+
1583
+ render json: order # Response not delayed ✅
1584
+ end
1585
+ ```
1586
+
1587
+ ### Measurement & Monitoring
1588
+
1589
+ **How to Verify SLOs in Your App:**
1590
+
1591
+ ```ruby
1592
+ # 1. Enable self-monitoring
1593
+ E11y.configure do |config|
1594
+ config.self_monitoring do
1595
+ enabled true
1596
+
1597
+ # Track E11y's own performance
1598
+ histogram :track_latency_ms,
1599
+ comment: 'Event track() call latency',
1600
+ buckets: [0.1, 0.5, 1, 2, 5, 10]
1601
+
1602
+ gauge :memory_usage_mb,
1603
+ comment: 'E11y memory footprint (RSS)'
1604
+
1605
+ counter :events_tracked_total,
1606
+ comment: 'Total events tracked'
1607
+ end
1608
+ end
1609
+
1610
+ # 2. Query SLOs in Prometheus
1611
+ # p99 track latency (should be <1ms)
1612
+ histogram_quantile(0.99,
1613
+ rate(e11y_track_latency_ms_bucket[5m])
1614
+ )
1615
+
1616
+ # Memory usage (should be <100MB)
1617
+ e11y_memory_usage_mb
1618
+
1619
+ # Throughput (events/sec)
1620
+ rate(e11y_events_tracked_total[1m])
1621
+ ```
1622
+
1623
+ **Alerting Rules:**
1624
+
1625
+ ```yaml
1626
+ # prometheus/alerts.yml
1627
+ groups:
1628
+ - name: e11y_slo
1629
+ rules:
1630
+ - alert: E11yHighLatency
1631
+ expr: histogram_quantile(0.99, rate(e11y_track_latency_ms_bucket[5m])) > 1
1632
+ for: 5m
1633
+ annotations:
1634
+ summary: "E11y p99 latency >1ms (SLO violation)"
1635
+
1636
+ - alert: E11yHighMemory
1637
+ expr: e11y_memory_usage_mb > 100
1638
+ for: 10m
1639
+ annotations:
1640
+ summary: "E11y memory usage >100MB (SLO violation)"
1641
+
1642
+ - alert: E11yLowThroughput
1643
+ expr: rate(e11y_events_tracked_total[1m]) < 1000 and rate(app_requests_total[1m]) > 1000
1644
+ annotations:
1645
+ summary: "E11y can't keep up with event load"
1646
+ ```
1647
+
1648
+ ### What If SLOs are Not Met?
1649
+
1650
+ **Common Causes & Solutions:**
1651
+
1652
+ | Symptom | Likely Cause | Solution |
1653
+ |---------|--------------|----------|
1654
+ | **p99 >1ms** | Too many events in buffer | Increase flush interval or reduce event volume |
1655
+ | **Memory >100MB** | Request buffer limit too high | Reduce `buffer_limit` (default: 100) |
1656
+ | **Throughput <1000/s** | Adapter bottleneck | Check adapter latency, enable batching |
1657
+ | **CPU >5%** | Expensive middleware | Profile middleware, optimize or remove |
1658
+
1659
+ **Debugging Performance Issues:**
1660
+
1661
+ ```ruby
1662
+ # Enable detailed profiling
1663
+ E11y.configure do |config|
1664
+ config.profiling do
1665
+ enabled true # Production: false (overhead!)
1666
+
1667
+ # Profile middleware latency
1668
+ profile_middleware true
1669
+
1670
+ # Profile adapter write latency
1671
+ profile_adapters true
1672
+ end
1673
+ end
1674
+
1675
+ # Check profiling results
1676
+ E11y::Profiler.report
1677
+ # Output:
1678
+ # Middleware Latency:
1679
+ # TraceContext: 0.05ms (5%)
1680
+ # Validation: 0.20ms (20%)
1681
+ # PiiFilter: 0.30ms (30%) ← Bottleneck!
1682
+ # Routing: 0.10ms (10%)
1683
+ #
1684
+ # Adapter Latency:
1685
+ # LokiAdapter: 15ms (avg)
1686
+ # SentryAdapter: 25ms (avg)
1687
+ ```
1688
+
1689
+ ### Performance Best Practices
1690
+
1691
+ **✅ DO:**
1692
+ - Keep event payload small (<1KB per event)
1693
+ - Use batching for high-volume events
1694
+ - Monitor E11y's own metrics
1695
+ - Set reasonable `buffer_limit` (50-100)
1696
+
1697
+ **❌ DON'T:**
1698
+ - Track >10,000 unique events/sec (scale horizontally instead)
1699
+ - Create middleware with blocking I/O (use async adapters)
1700
+ - Set `flush_interval` <50ms (too aggressive)
1701
+ - Disable batching for high-volume adapters
1702
+
1703
+ ---
1704
+
1705
+ ## 🔒 Validations (NEW - v1.1)
1706
+
1707
+ > **🎯 Pattern:** Validate configuration at class load time (fail fast!).
1708
+
1709
+ ### Schema Presence Validation
1710
+
1711
+ **Problem:** Events without schema → runtime errors.
1712
+
1713
+ **Solution:** Validate schema presence at class load:
1714
+
1715
+ ```ruby
1716
+ # Gem implementation (automatic):
1717
+ class E11y::Event::Base
1718
+ def self.inherited(subclass)
1719
+ super
1720
+ at_exit do
1721
+ unless subclass.respond_to?(:schema) && subclass.schema
1722
+ raise "#{subclass} missing schema! All events must define schema."
1723
+ end
1724
+ end
1725
+ end
1726
+ end
1727
+
1728
+ # Result:
1729
+ class Events::OrderPaid < E11y::Event::Base
1730
+ # ← ERROR at load time: "Events::OrderPaid missing schema!"
1731
+ end
1732
+ ```
1733
+
1734
+ ### Severity Level Validation
1735
+
1736
+ **Problem:** Typos in severity levels → silent failures.
1737
+
1738
+ **Solution:** Validate severity against whitelist:
1739
+
1740
+ ```ruby
1741
+ # Gem implementation (automatic):
1742
+ VALID_SEVERITIES = [:debug, :info, :success, :warn, :error, :fatal]
1743
+
1744
+ def self.severity(level)
1745
+ unless VALID_SEVERITIES.include?(level)
1746
+ raise ArgumentError, "Invalid severity: #{level}. Valid: #{VALID_SEVERITIES.join(', ')}"
1747
+ end
1748
+ self._severity = level
1749
+ end
1750
+
1751
+ # Result:
1752
+ class Events::OrderPaid < E11y::Event::Base
1753
+ severity :critical # ← ERROR: "Invalid severity: :critical. Valid: debug, info, success, warn, error, fatal"
1754
+ end
1755
+ ```
1756
+
1757
+ ### Adapters Registration Validation
1758
+
1759
+ **Problem:** Typos in adapter names → events lost.
1760
+
1761
+ **Solution:** Validate adapters against registered list:
1762
+
1763
+ ```ruby
1764
+ # Gem implementation (automatic):
1765
+ def self.adapters(list)
1766
+ list.each do |adapter|
1767
+ unless E11y.registered_adapters.include?(adapter)
1768
+ raise ArgumentError, "Unknown adapter: #{adapter}. Registered: #{E11y.registered_adapters.keys.join(', ')}"
1769
+ end
1770
+ end
1771
+ self._adapters = list
1772
+ end
1773
+
1774
+ # Result:
1775
+ class Events::OrderPaid < E11y::Event::Base
1776
+ adapters [:loki, :sentri] # ← ERROR: "Unknown adapter: :sentri. Registered: loki, sentry, file"
1777
+ end
1778
+ ```
1779
+
1780
+ ### Audit Event Locked Settings Validation
1781
+
1782
+ **Problem:** Overriding audit event settings → compliance violations.
1783
+
1784
+ **Solution:** Lock `rate_limiting` and `sampling` for audit events:
1785
+
1786
+ ```ruby
1787
+ # Gem implementation (automatic):
1788
+ def self.rate_limiting(enabled)
1789
+ if self._audit_event && enabled
1790
+ raise ArgumentError, "Cannot enable rate_limiting for audit events! Audit events must never be rate limited."
1791
+ end
1792
+ self._rate_limiting = enabled
1793
+ end
1794
+
1795
+ def self.sampling(enabled)
1796
+ if self._audit_event && enabled
1797
+ raise ArgumentError, "Cannot enable sampling for audit events! Audit events must never be sampled."
1798
+ end
1799
+ self._sampling = enabled
1800
+ end
1801
+
1802
+ # Result:
1803
+ class Events::UserDeleted < E11y::Event::Base
1804
+ audit_event true
1805
+ rate_limiting true # ← ERROR: "Cannot enable rate_limiting for audit events!"
1806
+ sampling true # ← ERROR: "Cannot enable sampling for audit events!"
1807
+ end
1808
+ ```
1809
+
1810
+ ---
1811
+
1812
+ ## 🌍 Environment-Specific Configuration (NEW - v1.1)
1813
+
1814
+ > **🎯 Pattern:** Use Ruby conditionals for environment-specific config.
1815
+
1816
+ ### Example 1: Debug Events (File in Dev, Loki in Prod)
1817
+
1818
+ ```ruby
1819
+ class Events::DebugQuery < E11y::Event::Base
1820
+ schema do
1821
+ required(:query).filled(:string)
1822
+ required(:duration_ms).filled(:integer)
1823
+ end
1824
+
1825
+ # Environment-specific adapters
1826
+ adapters Rails.env.production? ? [:loki] : [:file]
1827
+
1828
+ # Environment-specific sampling
1829
+ sample_rate Rails.env.production? ? 0.01 : 1.0 # 1% prod, 100% dev
1830
+ end
1831
+ ```
1832
+
1833
+ ### Example 2: High-Volume Events (Different Rates)
1834
+
1835
+ ```ruby
1836
+ class Events::ApiRequest < E11y::Event::Base
1837
+ schema do
1838
+ required(:endpoint).filled(:string)
1839
+ required(:status).filled(:integer)
1840
+ end
1841
+
1842
+ # Environment-specific rate limiting
1843
+ rate_limit case Rails.env
1844
+ when 'production' then 10_000
1845
+ when 'staging' then 1_000
1846
+ else 100
1847
+ end
1848
+ end
1849
+ ```
1850
+
1851
+ ### Example 3: Audit Retention (Jurisdiction-Specific)
1852
+
1853
+ ```ruby
1854
+ # config/initializers/e11y.rb
1855
+ E11y.configure do |config|
1856
+ # Configurable audit retention (GDPR: 7 years, other: custom)
1857
+ config.audit_retention = case ENV['JURISDICTION']
1858
+ when 'EU' then 7.years
1859
+ when 'US' then 10.years # Financial regulations
1860
+ else 5.years
1861
+ end
1862
+ end
1863
+
1864
+ # Event uses configured value:
1865
+ class Events::UserDeleted < E11y::Event::Base
1866
+ audit_event true
1867
+ # ← Auto: retention = E11y.config.audit_retention (7/10/5 years)
1868
+ end
1869
+ ```
1870
+
1871
+ ---
1872
+
1873
+ ## 📊 Precedence Rules (NEW - v1.1)
1874
+
1875
+ > **🎯 Pattern:** Configuration precedence (most specific wins).
1876
+
1877
+ ### Precedence Order (Highest to Lowest)
1878
+
1879
+ ```
1880
+ 1. Event-level explicit config (highest priority)
1881
+
1882
+ 2. Preset module config
1883
+
1884
+ 3. Base class config (inheritance)
1885
+
1886
+ 4. Convention-based defaults
1887
+
1888
+ 5. Global config (lowest priority)
1889
+ ```
1890
+
1891
+ ### Example: Mixing Inheritance + Presets
1892
+
1893
+ ```ruby
1894
+ # Global config (lowest priority)
1895
+ E11y.configure do |config|
1896
+ config.adapters = [:file] # Default for all events
1897
+ config.sample_rate = 0.1 # 10% default
1898
+ end
1899
+
1900
+ # Base class (medium priority)
1901
+ class Events::BasePaymentEvent < E11y::Event::Base
1902
+ severity :success
1903
+ adapters [:loki, :sentry] # Override global
1904
+ sample_rate 1.0 # Never sample payments
1905
+ end
1906
+
1907
+ # Preset module (higher priority)
1908
+ module E11y::Presets::HighValueEvent
1909
+ extend ActiveSupport::Concern
1910
+ included do
1911
+ rate_limit 10_000
1912
+ retention 7.years
1913
+ # Does NOT override adapters/sample_rate (not defined in preset)
1914
+ end
1915
+ end
1916
+
1917
+ # Event (highest priority)
1918
+ class Events::CriticalPayment < Events::BasePaymentEvent
1919
+ include E11y::Presets::HighValueEvent
1920
+
1921
+ adapters [:loki, :sentry, :s3_archive] # Override base (add S3)
1922
+
1923
+ # Final config:
1924
+ # - severity: :success (from base)
1925
+ # - adapters: [:loki, :sentry, :s3_archive] (event-level override)
1926
+ # - sample_rate: 1.0 (from base)
1927
+ # - rate_limit: 10_000 (from preset)
1928
+ # - retention: 7.years (from preset)
1929
+ end
1930
+ ```
1931
+
1932
+ ### Precedence Rules Table
1933
+
1934
+ | Config | Global | Convention | Base Class | Preset | Event-Level | Winner |
1935
+ |--------|--------|------------|------------|--------|-------------|--------|
1936
+ | `severity` | - | `:success` | `:warn` | - | `:error` | **`:error`** (event) |
1937
+ | `adapters` | `[:file]` | `[:loki]` | `[:sentry]` | - | - | **`[:sentry]`** (base) |
1938
+ | `sample_rate` | `0.1` | `0.5` | - | `1.0` | - | **`1.0`** (preset) |
1939
+ | `rate_limit` | `1000` | - | - | - | - | **`1000`** (global) |
1940
+
1941
+ ---
1942
+
1943
+ ## 📚 Related Use Cases
1944
+
1945
+ - **[UC-001: Request-Scoped Debug Buffering](./UC-001-request-scoped-debug-buffering.md)** - Debug vs business events
1946
+ - **[UC-003: Pattern-Based Metrics](./UC-003-pattern-based-metrics.md)** - Auto-generate metrics
1947
+ - **[UC-005: PII Filtering](./UC-005-pii-filtering.md)** - Secure event data
1948
+
1949
+ ---
1950
+
1951
+ **Document Version:** 1.0
1952
+ **Last Updated:** January 12, 2026
1953
+ **Status:** ✅ Complete