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,1061 @@
1
+ # ADR-015: Middleware Execution Order
2
+
3
+ **Status:** Stable
4
+ **Date:** January 13, 2026
5
+ **Covers:** Pipeline execution order, event versioning integration
6
+ **Depends On:** ADR-001 (Architecture), ADR-012 (Event Evolution)
7
+
8
+ ---
9
+
10
+ ## 📋 Table of Contents
11
+
12
+ 1. [Context & Problem](#1-context--problem)
13
+ 2. [Decision](#2-decision)
14
+ 3. [Correct Order](#3-correct-order)
15
+ - 3.1. Pipeline Flow
16
+ - 3.2. Why Each Middleware Needs Original Class Name
17
+ - 3.3. Audit Event Pipeline Separation (C01 Resolution) ⚠️ CRITICAL
18
+ - 3.3.1. The Problem: PII Filtering Breaks Audit Trail
19
+ - 3.3.2. Decision: Two Pipeline Configurations
20
+ - 3.3.3. Declaring Audit Events
21
+ - 3.3.4. Pipeline Configuration
22
+ - 3.3.5. Audit Signing Middleware Implementation
23
+ - 3.3.6. Encrypted Audit Adapter (C01 Requirement)
24
+ - 3.3.7. Usage Examples
25
+ - 3.3.8. Trade-offs & Security (C01)
26
+ - 3.4. Middleware Zones & Modification Rules (C19 Resolution) ⚠️ CRITICAL
27
+ - 3.4.1. The Problem: Uncontrolled Middleware Modifications
28
+ - 3.4.2. Decision: Middleware Zones
29
+ - 3.4.3. Zone-Based Configuration
30
+ - 3.4.4. Custom Middleware Constraints
31
+ - 3.4.5. Zone Validation (Runtime Checks)
32
+ - 3.4.6. Warning System for Violations
33
+ - 3.4.7. Examples: Safe vs Unsafe Middleware
34
+ - 3.4.8. Trade-offs & Guidelines (C19)
35
+ 4. [Wrong Order Example](#4-wrong-order-example)
36
+ 5. [Real-World Example](#5-real-world-example)
37
+ 6. [Implementation Checklist](#6-implementation-checklist)
38
+ 7. [See Also](#7-see-also)
39
+
40
+ ---
41
+
42
+ ## 1. Context & Problem
43
+
44
+ ### 1.1. Problem Statement
45
+
46
+ **Versioning Middleware normalizes event names for adapters, but when should this happen?**
47
+
48
+ ```ruby
49
+ # Events::OrderPaidV2.track(...)
50
+ # Should validation use "Events::OrderPaidV2" or "Events::OrderPaid"?
51
+ # Should PII filtering use V2 rules or V1 rules?
52
+ # When do we normalize the name?
53
+ ```
54
+
55
+ **Wrong placement breaks business logic:**
56
+ - ❌ Too early → Validation fails (can't find V2 schema)
57
+ - ❌ Too early → PII filtering uses wrong rules
58
+ - ❌ Too early → Rate limiting uses wrong limits
59
+
60
+ ### 1.2. Key Insight
61
+
62
+ > **Versioning = Cosmetic normalization for external systems**
63
+ > **All business logic MUST use original class name**
64
+
65
+ ---
66
+
67
+ ## 2. Decision
68
+
69
+ **Versioning Middleware MUST be LAST (before routing to adapters)**
70
+
71
+ ```ruby
72
+ # config/initializers/e11y.rb
73
+ E11y.configure do |config|
74
+ config.middleware.use E11y::Middleware::TraceContext # 1
75
+ config.middleware.use E11y::Middleware::Validation # 2
76
+ config.middleware.use E11y::Middleware::PIIFiltering # 3
77
+ config.middleware.use E11y::Middleware::RateLimiting # 4
78
+ config.middleware.use E11y::Middleware::Sampling # 5
79
+ config.middleware.use E11y::Middleware::Versioning # 6 ← LAST!
80
+ config.middleware.use E11y::Middleware::Routing # 7
81
+ end
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 3. Correct Order
87
+
88
+ ### 3.1. Pipeline Flow
89
+
90
+ ```
91
+ Events::OrderPaidV2.track(order_id: 123, amount: 99.99)
92
+
93
+ 1. TraceContext → Add trace_id, span_id, timestamp
94
+ event_name = "Events::OrderPaidV2" (original)
95
+
96
+ 2. Validation → Uses Events::OrderPaidV2 schema ✅
97
+ event_name = "Events::OrderPaidV2" (original)
98
+
99
+ 3. PII Filtering → Uses Events::OrderPaidV2 PII rules ✅
100
+ event_name = "Events::OrderPaidV2" (original)
101
+
102
+ 4. Rate Limiting → Checks limit for "Events::OrderPaidV2" ✅
103
+ event_name = "Events::OrderPaidV2" (original)
104
+
105
+ 5. Sampling → Checks sample rate for "Events::OrderPaidV2" ✅
106
+ event_name = "Events::OrderPaidV2" (original)
107
+
108
+ 6. Versioning → Normalize: "Events::OrderPaid"
109
+ (LAST!) Add v: 2 to payload
110
+ event_name = "Events::OrderPaid" (normalized)
111
+
112
+ 7. Routing → Route to buffer
113
+ event_name = "Events::OrderPaid" (normalized)
114
+
115
+ Adapters → Receive normalized name
116
+ event_name = "Events::OrderPaid"
117
+ payload: { v: 2, order_id: 123, ... }
118
+ ```
119
+
120
+ ### 3.2. Why Each Middleware Needs Original Class Name
121
+
122
+ | Middleware | Needs Original? | Why? |
123
+ |------------|----------------|------|
124
+ | **TraceContext** | No | Just adds trace_id, doesn't care about class |
125
+ | **Validation** | ✅ Yes | Schema is attached to specific class (V2 ≠ V1) |
126
+ | **PIIFiltering** | ✅ Yes | PII rules may differ between V1 and V2 |
127
+ | **RateLimiting** | ✅ Yes | Rate limits may differ between V1 and V2 |
128
+ | **Sampling** | ✅ Yes | Sample rates may differ between V1 and V2 |
129
+ | **Versioning** | No | Normalizes for adapters (cosmetic change) |
130
+ | **Routing** | No | Routes based on severity, not class name |
131
+ | **Adapters** | No | Prefer normalized name (easier querying) |
132
+
133
+ ---
134
+
135
+ ## 4. Wrong Order Example
136
+
137
+ ### 4.1. Versioning First (WRONG!)
138
+
139
+ ```ruby
140
+ # ❌ WRONG ORDER!
141
+ config.middleware.use E11y::Middleware::Versioning # 1 ← Too early!
142
+ config.middleware.use E11y::Middleware::Validation # 2
143
+ config.middleware.use E11y::Middleware::PIIFiltering # 3
144
+ config.middleware.use E11y::Middleware::RateLimiting # 4
145
+ config.middleware.use E11y::Middleware::Sampling # 5
146
+ ```
147
+
148
+ ### 4.2. What Breaks
149
+
150
+ ```ruby
151
+ Events::OrderPaidV2.track(...)
152
+
153
+ 1. Versioning: Normalize "Events::OrderPaidV2" → "Events::OrderPaid"
154
+
155
+ 2. Validation: ❌ Can't find schema for "Events::OrderPaid" (was V2!)
156
+
157
+ 3. PII Filtering: ❌ Uses V1 rules instead of V2 rules!
158
+
159
+ 4. Rate Limiting: ❌ Uses V1 limit instead of V2 limit!
160
+
161
+ 5. Sampling: ❌ Uses V1 sample rate instead of V2 rate!
162
+ ```
163
+
164
+ ---
165
+
166
+ ## 5. Real-World Example
167
+
168
+ ```ruby
169
+ # V1: Old version (production)
170
+ class Events::OrderPaid < E11y::Event::Base
171
+ schema do
172
+ required(:order_id).filled(:integer)
173
+ required(:amount).filled(:float)
174
+ # No currency field!
175
+ end
176
+
177
+ pii_filtering do
178
+ masks :email # V1: masks email
179
+ end
180
+
181
+ adapters :loki, :sentry
182
+ severity :info
183
+ end
184
+
185
+ # V2: New version (A/B test, 10% traffic)
186
+ class Events::OrderPaidV2 < E11y::Event::Base
187
+ schema do
188
+ required(:order_id).filled(:integer)
189
+ required(:amount).filled(:float)
190
+ required(:currency).filled(:string) # ← NEW FIELD!
191
+ end
192
+
193
+ pii_filtering do
194
+ hashes :email # V2: hashes email (different rule!)
195
+ end
196
+
197
+ adapters :loki, :sentry
198
+ severity :info
199
+ end
200
+
201
+ # Rate limiting config
202
+ E11y.configure do |config|
203
+ config.rate_limiting do
204
+ per_event 'Events::OrderPaid', limit: 1000, window: 1.second # V1: high limit
205
+ per_event 'Events::OrderPaidV2', limit: 100, window: 1.second # V2: low limit (A/B test)
206
+ end
207
+ end
208
+
209
+ # Pipeline execution:
210
+ Events::OrderPaidV2.track(order_id: 123, amount: 99.99, currency: 'USD')
211
+
212
+ 1. Validation: ✅ Uses V2 schema (checks currency field exists)
213
+ 2. PII Filtering: ✅ Uses V2 rules (hashes email, not masks)
214
+ 3. Rate Limiting: ✅ Uses V2 limit (100 req/sec, not 1000)
215
+ 4. Sampling: ✅ Uses V2 sample rate (if configured differently)
216
+ 5. Versioning: Normalize to "Events::OrderPaid", add v: 2
217
+ 6. Routing: Route to main buffer
218
+
219
+ Loki receives:
220
+ {
221
+ event_name: "Events::OrderPaid", ← Normalized!
222
+ v: 2, ← Version explicit
223
+ order_id: 123,
224
+ amount: 99.99,
225
+ currency: "USD",
226
+ email: "sha256:abc123..." ← Hashed (V2 rule)
227
+ }
228
+
229
+ # Easy querying in Loki:
230
+ # All versions: {event_name="Events::OrderPaid"}
231
+ # Only V2: {event_name="Events::OrderPaid", v="2"}
232
+ # Only V1: {event_name="Events::OrderPaid"} |= "" != "v"
233
+ ```
234
+
235
+ ---
236
+
237
+ ## 6. Implementation Checklist
238
+
239
+ - [ ] Versioning Middleware is **LAST** (before Routing)
240
+ - [ ] All business logic middleware uses **ORIGINAL class name**
241
+ - [ ] Adapters receive **NORMALIZED event_name**
242
+ - [ ] `v:` field is added **only if version > 1**
243
+ - [ ] Rate limits are configured **per original class** (if differ)
244
+ - [ ] PII rules are configured **per original class** (if differ)
245
+ - [ ] Sampling rules are configured **per original class** (if differ)
246
+ - [ ] Metrics track **both** normalized name and version
247
+ - [ ] Audit events use separate pipeline (C01 - see §3.3)
248
+ - [ ] Audit events stored in encrypted adapter (C01 requirement)
249
+
250
+ ---
251
+
252
+ ## 3.3. Audit Event Pipeline Separation (C01 Resolution)
253
+
254
+ > **⚠️ CRITICAL: C01 Conflict Resolution - PII Filtering × Audit Trail Signing**
255
+ > **See:** [CONFLICT-ANALYSIS.md C01](researches/CONFLICT-ANALYSIS.md#c01-pii-filtering--audit-trail-signing) for detailed analysis
256
+ > **Problem:** PII filtering before signing breaks non-repudiation (auditors can't verify original event)
257
+ > **Solution:** Separate pipeline for audit events that skips PII filtering, signs original data
258
+
259
+ ### 3.3.1. The Problem: PII Filtering Breaks Audit Trail
260
+
261
+ **Standard pipeline:**
262
+ ```ruby
263
+ Event.track(email: 'user@example.com', ip: '192.168.1.1')
264
+
265
+ PII Filtering → { email: '[FILTERED]', ip: '[FILTERED]' }
266
+
267
+ Audit Signing → HMAC-SHA256('[FILTERED]' data)
268
+
269
+ Storage
270
+
271
+ # ❌ Problem: Signature is based on FILTERED data!
272
+ # Auditor cannot verify original event was not tampered with
273
+ # Non-repudiation requirement VIOLATED
274
+ ```
275
+
276
+ **Legal Requirements:**
277
+ - **Non-repudiation:** Must prove event content hasn't been altered since creation
278
+ - **Audit trail:** Must maintain cryptographic chain of custody
279
+ - **Forensics:** Must be able to reconstruct exact event that occurred
280
+
281
+ ### 3.3.2. Decision: Two Pipeline Configurations
282
+
283
+ **Standard Events (Non-Audit):**
284
+ ```
285
+ 1. TraceContext → Add trace_id, span_id, timestamp
286
+ 2. Validation → Schema validation (original class)
287
+ 3. PIIFiltering → Filter PII EARLY ✅
288
+ 4. RateLimiting → Rate limit check
289
+ 5. Sampling → Adaptive sampling
290
+ 6. Versioning → Normalize event_name (LAST)
291
+ 7. Routing → Route to buffer
292
+ ```
293
+
294
+ **Audit Events (Legal Compliance):**
295
+ ```
296
+ 1. TraceContext → Add trace_id, span_id, timestamp
297
+ 2. Validation → Schema validation (original class)
298
+ 3. AuditSigning → Sign ORIGINAL data (includes PII!) ✅
299
+ 4. Versioning → Normalize event_name (LAST)
300
+ 5. Routing → Route to audit buffer
301
+
302
+ ❌ NO PII filtering for audit events!
303
+ ❌ NO rate limiting for audit events!
304
+ ❌ NO sampling for audit events!
305
+ ```
306
+
307
+ ### 3.3.3. Declaring Audit Events
308
+
309
+ **Event Class Flag:**
310
+ ```ruby
311
+ # Audit event - uses audit pipeline
312
+ class Events::PermissionChanged < E11y::Event::Base
313
+ audit_event true # ← Trigger audit pipeline
314
+ # Auto-set: retention = E11y.config.audit_retention (configurable!)
315
+ # rate_limiting = false (LOCKED!)
316
+ # sampling = false (LOCKED!)
317
+
318
+ schema do
319
+ required(:user_id).filled(:string)
320
+ required(:admin_email).filled(:string) # ← PII preserved for audit!
321
+ required(:changed_by).filled(:string)
322
+ required(:old_role).filled(:string)
323
+ required(:new_role).filled(:string)
324
+ required(:timestamp).filled(:time)
325
+ end
326
+
327
+ # Audit-specific configuration
328
+ adapters :audit_encrypted # ← MUST use encrypted storage
329
+ severity :warn
330
+ version 1
331
+ end
332
+
333
+ # Standard event - uses standard pipeline
334
+ class Events::PageView < E11y::Event::Base
335
+ audit_event false # ← Use standard pipeline (default)
336
+
337
+ schema do
338
+ required(:user_id).filled(:string)
339
+ required(:email).filled(:string) # ← PII will be filtered
340
+ required(:page_url).filled(:string)
341
+ end
342
+
343
+ pii_filtering do
344
+ masks :email # Applied early in pipeline
345
+ end
346
+
347
+ adapters :loki, :elasticsearch
348
+ severity :info
349
+ version 1
350
+ end
351
+ ```
352
+
353
+ ### 3.3.4. Pipeline Configuration
354
+
355
+ ```ruby
356
+ # config/initializers/e11y.rb
357
+ E11y.configure do |config|
358
+ # Standard pipeline (default for most events)
359
+ config.pipeline.use E11y::Middleware::TraceContext # 1
360
+ config.pipeline.use E11y::Middleware::Validation # 2
361
+ config.pipeline.use E11y::Middleware::PIIFiltering # 3
362
+ config.pipeline.use E11y::Middleware::RateLimiting # 4
363
+ config.pipeline.use E11y::Middleware::Sampling # 5
364
+ config.pipeline.use E11y::Middleware::Versioning # 6
365
+ config.pipeline.use E11y::Middleware::Routing # 7
366
+
367
+ # Audit pipeline override (for audit_event: true)
368
+ config.audit_pipeline.use E11y::Middleware::TraceContext # 1
369
+ config.audit_pipeline.use E11y::Middleware::Validation # 2
370
+ config.audit_pipeline.use E11y::Middleware::AuditSigning # 3 (NEW!)
371
+ config.audit_pipeline.use E11y::Middleware::Versioning # 4
372
+ config.audit_pipeline.use E11y::Middleware::AuditRouting # 5
373
+
374
+ # Audit event configuration
375
+ config.audit_events do
376
+ enabled true
377
+
378
+ # Signing configuration (HMAC-SHA256)
379
+ signing do
380
+ algorithm :hmac_sha256
381
+ secret_key ENV['E11Y_AUDIT_SECRET_KEY'] # ← Must be set!
382
+
383
+ # Include all fields in signature
384
+ include_fields :all
385
+
386
+ # Add signature metadata
387
+ add_signature_metadata true # timestamp, key_id, algorithm
388
+ end
389
+
390
+ # Storage requirement (C01)
391
+ storage do
392
+ encrypted true # ← MANDATORY for audit events with PII
393
+ adapter :audit_encrypted # Use encrypted storage adapter
394
+ end
395
+ end
396
+ end
397
+ ```
398
+
399
+ ### 3.3.5. Audit Signing Middleware Implementation
400
+
401
+ ```ruby
402
+ module E11y
403
+ module Middleware
404
+ class AuditSigning < Base
405
+ def call(event_data)
406
+ # Only sign audit events
407
+ unless event_data[:audit_event]
408
+ return @app.call(event_data)
409
+ end
410
+
411
+ # Generate signature payload (includes ALL fields, including PII)
412
+ signature_payload = build_signature_payload(event_data)
413
+
414
+ # Calculate HMAC-SHA256 signature
415
+ secret_key = Config.audit_events.signing.secret_key
416
+ signature = OpenSSL::HMAC.hexdigest(
417
+ 'SHA256',
418
+ secret_key,
419
+ signature_payload
420
+ )
421
+
422
+ # Add signature to event
423
+ event_data[:audit_signature] = {
424
+ value: signature,
425
+ algorithm: 'HMAC-SHA256',
426
+ timestamp: Time.now.utc.iso8601(3),
427
+ key_id: Config.audit_events.signing.key_id || 'default',
428
+ payload_hash: Digest::SHA256.hexdigest(signature_payload)
429
+ }
430
+
431
+ # Mark as signed
432
+ event_data[:audit_signed] = true
433
+
434
+ # Metrics
435
+ Metrics.increment('e11y.audit.events_signed')
436
+
437
+ @app.call(event_data)
438
+ end
439
+
440
+ private
441
+
442
+ def build_signature_payload(event_data)
443
+ # Canonical representation for signature
444
+ # (sorted keys, consistent JSON formatting)
445
+ payload_fields = {
446
+ event_name: event_data[:event_name],
447
+ event_version: event_data[:event_version],
448
+ timestamp: event_data[:timestamp].iso8601(3),
449
+ trace_id: event_data[:trace_id],
450
+ payload: event_data[:payload]
451
+ }
452
+
453
+ # Sort keys for deterministic signature
454
+ JSON.generate(payload_fields.deep_sort_by_key)
455
+ end
456
+ end
457
+ end
458
+ end
459
+ ```
460
+
461
+ ### 3.3.6. Encrypted Audit Adapter (C01 Requirement)
462
+
463
+ **Why Encryption is Mandatory:**
464
+ - Audit events contain **PII** (not filtered)
465
+ - Signature is on **original data** (including PII)
466
+ - Storage must protect PII at rest (GDPR compliance)
467
+
468
+ ```ruby
469
+ # lib/e11y/adapters/audit_encrypted_adapter.rb
470
+ module E11y
471
+ module Adapters
472
+ class AuditEncryptedAdapter < Base
473
+ def initialize(storage_path:, encryption_key:)
474
+ @storage_path = storage_path
475
+ @cipher = OpenSSL::Cipher.new('AES-256-GCM')
476
+ @encryption_key = encryption_key
477
+ end
478
+
479
+ def write_batch(events)
480
+ events.each do |event_data|
481
+ # Verify audit signature
482
+ unless verify_signature(event_data)
483
+ Rails.logger.error "[E11y] Audit signature verification failed: #{event_data[:event_name]}"
484
+ Metrics.increment('e11y.audit.signature_verification_failed')
485
+ next
486
+ end
487
+
488
+ # Encrypt event (AES-256-GCM)
489
+ encrypted_payload = encrypt(event_data)
490
+
491
+ # Store encrypted event
492
+ File.open("#{@storage_path}/audit_#{event_data[:trace_id]}.enc", 'wb') do |f|
493
+ f.write(encrypted_payload)
494
+ end
495
+
496
+ Metrics.increment('e11y.audit.events_stored')
497
+ end
498
+ end
499
+
500
+ private
501
+
502
+ def encrypt(event_data)
503
+ @cipher.encrypt
504
+ @cipher.key = @encryption_key
505
+ iv = @cipher.random_iv
506
+
507
+ encrypted_data = @cipher.update(JSON.generate(event_data)) + @cipher.final
508
+ auth_tag = @cipher.auth_tag
509
+
510
+ # Prepend IV and auth_tag for decryption
511
+ [iv, auth_tag, encrypted_data].map { |x| [x].pack('m0') }.join("\n")
512
+ end
513
+
514
+ def verify_signature(event_data)
515
+ signature_data = event_data[:audit_signature]
516
+ return false unless signature_data
517
+
518
+ # Rebuild signature payload
519
+ payload = build_signature_payload(event_data)
520
+
521
+ # Verify signature
522
+ expected_sig = OpenSSL::HMAC.hexdigest(
523
+ 'SHA256',
524
+ Config.audit_events.signing.secret_key,
525
+ payload
526
+ )
527
+
528
+ signature_data[:value] == expected_sig
529
+ end
530
+ end
531
+ end
532
+ end
533
+ ```
534
+
535
+ ### 3.3.7. Usage Examples
536
+
537
+ **Tracking Audit Event:**
538
+ ```ruby
539
+ # Audit event - PII preserved, signature added
540
+ Events::PermissionChanged.track(
541
+ user_id: 'user-123',
542
+ admin_email: 'admin@company.com', # ← PII preserved!
543
+ changed_by: 'admin-456',
544
+ old_role: 'viewer',
545
+ new_role: 'editor',
546
+ resource_type: 'document',
547
+ resource_id: 'doc-789'
548
+ )
549
+
550
+ # Pipeline execution:
551
+ # 1. TraceContext → Add trace_id: 'abc-def-123'
552
+ # 2. Validation → Schema check ✅
553
+ # 3. AuditSigning → Calculate HMAC-SHA256 signature on ORIGINAL data
554
+ # 4. Versioning → Normalize event_name
555
+ # 5. AuditRouting → Route to audit buffer
556
+ # 6. AuditEncryptedAdapter → Encrypt and store
557
+
558
+ # Stored event (encrypted):
559
+ {
560
+ event_name: "permission.changed",
561
+ event_version: 1,
562
+ trace_id: "abc-def-123",
563
+ timestamp: "2026-01-14T10:30:45.123Z",
564
+ payload: {
565
+ user_id: "user-123",
566
+ admin_email: "admin@company.com", # ← PII in signed payload!
567
+ changed_by: "admin-456",
568
+ old_role: "viewer",
569
+ new_role: "editor"
570
+ },
571
+ audit_signature: {
572
+ value: "a1b2c3d4e5f6...", # ← Signature on ORIGINAL data
573
+ algorithm: "HMAC-SHA256",
574
+ timestamp: "2026-01-14T10:30:45.123Z",
575
+ key_id: "default",
576
+ payload_hash: "sha256:abc123..."
577
+ }
578
+ }
579
+ ```
580
+
581
+ **Verifying Audit Trail:**
582
+ ```ruby
583
+ # Forensic verification
584
+ audit_event = E11y::AuditLog.find_by(trace_id: 'abc-def-123')
585
+
586
+ # 1. Decrypt event
587
+ decrypted_event = E11y::AuditEncryptedAdapter.decrypt(audit_event.encrypted_payload)
588
+
589
+ # 2. Verify signature
590
+ signature_valid = E11y::AuditSigning.verify(
591
+ decrypted_event,
592
+ secret_key: ENV['E11Y_AUDIT_SECRET_KEY']
593
+ )
594
+
595
+ # 3. Reconstruct payload
596
+ if signature_valid
597
+ puts "✅ Audit event verified:"
598
+ puts " Original email: #{decrypted_event[:payload][:admin_email]}"
599
+ puts " Signature: VALID"
600
+ puts " Timestamp: #{decrypted_event[:audit_signature][:timestamp]}"
601
+ else
602
+ puts "❌ Audit event signature INVALID - event may have been tampered!"
603
+ end
604
+ ```
605
+
606
+ ### 3.3.8. Trade-offs & Security (C01)
607
+
608
+ **Trade-offs:**
609
+
610
+ | Aspect | Pro | Con | Mitigation |
611
+ |--------|-----|-----|------------|
612
+ | **Non-repudiation** | ✅ Signature on original data | ⚠️ PII in audit events | Use encrypted storage adapter |
613
+ | **Legal compliance** | ✅ Meets audit requirements | ⚠️ Two pipelines to maintain | Clear documentation |
614
+ | **PII protection** | ✅ Standard events filtered | ⚠️ Audit events not filtered | Restrict access to audit logs |
615
+ | **Performance** | ✅ No PII filter overhead | ⚠️ Signing + encryption overhead | Audit events are rare (<1%) |
616
+
617
+ **Security Requirements (C01):**
618
+
619
+ 1. **Encrypted Storage (Mandatory):**
620
+ - All audit events MUST be stored encrypted (AES-256-GCM)
621
+ - Encryption keys managed via secure key management (AWS KMS, HashiCorp Vault, etc.)
622
+ - Access to audit logs restricted to authorized personnel only
623
+
624
+ 2. **Access Control:**
625
+ - Audit log access requires multi-factor authentication (MFA)
626
+ - All audit log access must be logged (audit the auditors!)
627
+ - Role-based access control (RBAC) for audit log decryption
628
+
629
+ 3. **Key Rotation:**
630
+ - Signing keys rotated quarterly (or per company policy)
631
+ - Old signatures remain valid (use key_id to identify key version)
632
+ - Re-signing not required after key rotation (signatures remain valid)
633
+
634
+ **Monitoring (Critical for C01):**
635
+
636
+ ```ruby
637
+ # Prometheus/Yabeda metrics
638
+ Yabeda.configure do
639
+ group :e11y_audit do
640
+ counter :events_signed, comment: 'Audit events signed'
641
+ counter :events_stored, comment: 'Audit events stored (encrypted)'
642
+ counter :signature_verification_failed, comment: 'Signature verification failures'
643
+ counter :encryption_errors, comment: 'Encryption failures'
644
+
645
+ gauge :audit_log_size_bytes, comment: 'Total size of audit logs'
646
+ end
647
+ end
648
+
649
+ # Alert rules (Grafana)
650
+ # Alert: Signature verification failures > 0 (investigate immediately!)
651
+ # Alert: Encryption errors > 0 (check key configuration)
652
+ # Alert: Audit log size > 10 GB (consider archival)
653
+ ```
654
+
655
+ **Related Conflicts:**
656
+ - **C07:** DLQ replay with PII filtering (see ADR-013)
657
+ - **C19:** Pipeline modification rules (see §3.4 below)
658
+
659
+ ---
660
+
661
+ ## 3.4. Middleware Zones & Modification Rules (C19 Resolution)
662
+
663
+ > **⚠️ CRITICAL: C19 Conflict Resolution - Custom Middleware × Pipeline Integrity**
664
+ > **See:** [CONFLICT-ANALYSIS.md C19](researches/CONFLICT-ANALYSIS.md#c19-custom-middleware--pipeline-modification) for detailed analysis
665
+ > **Problem:** Custom middleware can bypass PII filtering or undo security modifications
666
+ > **Solution:** Define middleware zones with clear modification constraints
667
+
668
+ ### 3.4.1. The Problem: Uncontrolled Middleware Modifications
669
+
670
+ **Scenario - Accidental PII Bypass:**
671
+ ```ruby
672
+ # Developer adds custom middleware
673
+ config.middleware.insert_after :pii_filtering, CustomEnrichmentMiddleware
674
+
675
+ class CustomEnrichmentMiddleware
676
+ def call(event_data)
677
+ # Accidentally adds PII AFTER filtering!
678
+ event_data[:payload][:user_email] = Current.user.email # ← PII leak!
679
+
680
+ @app.call(event_data)
681
+ end
682
+ end
683
+
684
+ # Pipeline execution:
685
+ # 1. PIIFiltering → Removes :email field ✅
686
+ # 2. CustomEnrichment → Adds :user_email field ❌ PII bypass!
687
+ # 3. Adapters → Receive event with unfiltered PII!
688
+ ```
689
+
690
+ **Scenario - Modification Conflicts:**
691
+ ```ruby
692
+ # Multiple middlewares modifying same fields
693
+ # 1. PII Filtering → email: '[FILTERED]'
694
+ # 2. Trace Context → adds trace_id
695
+ # 3. Payload Minimization → abbreviates keys (email → em)
696
+ # 4. Custom Middleware → restores original email (?!)
697
+
698
+ # Result: Cascading modifications break invariants!
699
+ ```
700
+
701
+ ### 3.4.2. Decision: Middleware Zones
702
+
703
+ Middlewares are grouped into **zones** with clear **modification rules**:
704
+
705
+ ```
706
+ ┌─────────────────────────────────────────────────────────┐
707
+ │ ZONE 1: PRE-PROCESSING │
708
+ │ ├─ Validation ← Can REJECT event │
709
+ │ └─ Schema Enrichment ← Can ADD required fields │
710
+ │ │
711
+ │ Rules: │
712
+ │ - Can add missing fields (defaults, timestamps) │
713
+ │ - Can reject invalid events (raise error) │
714
+ │ - Cannot modify PII (too early!) │
715
+ └─────────────────────────────────────────────────────────┘
716
+
717
+ ┌─────────────────────────────────────────────────────────┐
718
+ │ ZONE 2: SECURITY (CRITICAL!) │
719
+ │ └─ PII Filtering ← Can MODIFY sensitive fields │
720
+ │ │
721
+ │ Rules: │
722
+ │ - LAST chance to touch PII fields │
723
+ │ - NO middleware after this can modify/add PII │
724
+ │ - Custom middleware CANNOT run after PII filtering │
725
+ └─────────────────────────────────────────────────────────┘
726
+
727
+ ┌─────────────────────────────────────────────────────────┐
728
+ │ ZONE 3: ROUTING │
729
+ │ ├─ Rate Limiting ← Can DROP event │
730
+ │ └─ Sampling ← Can DROP event │
731
+ │ │
732
+ │ Rules: │
733
+ │ - Can inspect event (read-only) │
734
+ │ - Can decide to drop event (return early) │
735
+ │ - Cannot modify payload │
736
+ └─────────────────────────────────────────────────────────┘
737
+
738
+ ┌─────────────────────────────────────────────────────────┐
739
+ │ ZONE 4: POST-PROCESSING │
740
+ │ ├─ Trace Context ← Can ADD non-PII tracing fields │
741
+ │ ├─ Versioning ← Can NORMALIZE event_name │
742
+ │ └─ Minimization ← Can ABBREVIATE keys (last step!) │
743
+ │ │
744
+ │ Rules: │
745
+ │ - Can add metadata (trace_id, timestamps) │
746
+ │ - Can transform structure (abbreviate keys) │
747
+ │ - Cannot add PII (already filtered!) │
748
+ └─────────────────────────────────────────────────────────┘
749
+
750
+ ┌─────────────────────────────────────────────────────────┐
751
+ │ ZONE 5: ADAPTERS │
752
+ │ ├─ Routing ← Route to buffer │
753
+ │ └─ Adapters ← Write to external systems │
754
+ │ │
755
+ │ Rules: │
756
+ │ - Read-only access to event │
757
+ │ - Cannot modify (too late!) │
758
+ └─────────────────────────────────────────────────────────┘
759
+ ```
760
+
761
+ ### 3.4.3. Zone-Based Configuration
762
+
763
+ ```ruby
764
+ # config/initializers/e11y.rb
765
+ E11y.configure do |config|
766
+ # ZONE 1: Pre-processing
767
+ config.pipeline.zone(:pre_processing) do
768
+ use E11y::Middleware::TraceContext # Add trace_id, timestamp
769
+ use E11y::Middleware::Validation # Schema validation
770
+ end
771
+
772
+ # ZONE 2: Security (CRITICAL - PII handled here!)
773
+ config.pipeline.zone(:security) do
774
+ use E11y::Middleware::PIIFiltering # ← LAST PII touchpoint!
775
+ end
776
+
777
+ # ZONE 3: Routing (read-only decision making)
778
+ config.pipeline.zone(:routing) do
779
+ use E11y::Middleware::RateLimiting # Can drop events
780
+ use E11y::Middleware::Sampling # Can drop events
781
+ end
782
+
783
+ # ZONE 4: Post-processing (metadata enrichment)
784
+ config.pipeline.zone(:post_processing) do
785
+ use E11y::Middleware::Versioning # Normalize event_name
786
+ use E11y::Middleware::PayloadMinimization # Abbreviate keys (last!)
787
+ end
788
+
789
+ # ZONE 5: Adapters (delivery)
790
+ config.pipeline.zone(:adapters) do
791
+ use E11y::Middleware::Routing # Buffer routing
792
+ end
793
+ end
794
+ ```
795
+
796
+ ### 3.4.4. Custom Middleware Constraints
797
+
798
+ **Safe Placement Options:**
799
+
800
+ ```ruby
801
+ # ✅ SAFE: Add custom middleware in pre-processing zone
802
+ config.pipeline.zone(:pre_processing) do
803
+ use E11y::Middleware::Validation
804
+ use MyCustomEnrichmentMiddleware # ← Before PII filtering (can add fields)
805
+ end
806
+
807
+ # ✅ SAFE: Add custom middleware in post-processing zone
808
+ config.pipeline.zone(:post_processing) do
809
+ use E11y::Middleware::Versioning
810
+ use MyCustomMetadataMiddleware # ← After PII filtering (metadata only!)
811
+ end
812
+
813
+ # ❌ UNSAFE: Cannot add middleware AFTER PII filtering but BEFORE post-processing
814
+ # This would create a PII bypass window!
815
+ ```
816
+
817
+ **Custom Middleware Template:**
818
+
819
+ ```ruby
820
+ class MyCustomEnrichmentMiddleware < E11y::Middleware
821
+ # Declare which zone this middleware belongs to
822
+ middleware_zone :pre_processing # or :post_processing
823
+
824
+ # Declare what modifications this middleware makes
825
+ modifies_fields :custom_metadata, :enrichment_data
826
+
827
+ def call(event_data)
828
+ # Validate zone constraints
829
+ validate_zone_rules!(event_data)
830
+
831
+ # Add custom fields (pre-processing zone)
832
+ if middleware_zone == :pre_processing
833
+ event_data[:payload][:custom_metadata] = fetch_metadata(event_data)
834
+ end
835
+
836
+ # Add metadata (post-processing zone)
837
+ if middleware_zone == :post_processing
838
+ event_data[:payload][:enrichment_timestamp] = Time.now.utc.iso8601(3)
839
+
840
+ # ⚠️ Cannot add PII fields here!
841
+ validate_no_pii_fields!(event_data[:payload])
842
+ end
843
+
844
+ @app.call(event_data)
845
+ end
846
+
847
+ private
848
+
849
+ def validate_zone_rules!(event_data)
850
+ # Ensure no PII fields added in post-processing zone
851
+ if middleware_zone == :post_processing
852
+ pii_patterns = Config.pii_filtering.field_patterns
853
+
854
+ event_data[:payload].keys.each do |key|
855
+ if pii_patterns.any? { |pattern| key.to_s.match?(pattern) }
856
+ raise E11y::ZoneViolationError,
857
+ "PII field '#{key}' cannot be added in post-processing zone! " \
858
+ "PII filtering already completed."
859
+ end
860
+ end
861
+ end
862
+ end
863
+ end
864
+ ```
865
+
866
+ ### 3.4.5. Zone Validation (Runtime Checks)
867
+
868
+ ```ruby
869
+ module E11y
870
+ class Pipeline
871
+ class << self
872
+ # Validate zone constraints at boot time
873
+ def validate_zones!
874
+ current_zone = :pre_processing
875
+
876
+ @middlewares.each do |middleware_class, args, options|
877
+ declared_zone = middleware_class.middleware_zone
878
+
879
+ # Check zone progression
880
+ unless valid_zone_transition?(current_zone, declared_zone)
881
+ raise E11y::InvalidPipelineError,
882
+ "Invalid middleware order: #{middleware_class} (zone: #{declared_zone}) " \
883
+ "cannot follow zone #{current_zone}. " \
884
+ "Valid order: pre_processing → security → routing → post_processing → adapters"
885
+ end
886
+
887
+ current_zone = declared_zone
888
+ end
889
+ end
890
+
891
+ private
892
+
893
+ def valid_zone_transition?(from_zone, to_zone)
894
+ zone_order = {
895
+ pre_processing: 1,
896
+ security: 2,
897
+ routing: 3,
898
+ post_processing: 4,
899
+ adapters: 5
900
+ }
901
+
902
+ zone_order[to_zone] >= zone_order[from_zone]
903
+ end
904
+ end
905
+ end
906
+ end
907
+
908
+ # Run at Rails boot
909
+ Rails.application.config.after_initialize do
910
+ E11y::Pipeline.validate_zones!
911
+ end
912
+ ```
913
+
914
+ ### 3.4.6. Warning System for Violations
915
+
916
+ ```ruby
917
+ # Development/staging environment warnings
918
+ if Rails.env.development? || Rails.env.staging?
919
+ E11y.configure do |config|
920
+ config.pipeline.enable_zone_warnings = true
921
+
922
+ # Warn if custom middleware added after PII filtering
923
+ config.pipeline.on_zone_violation do |violation|
924
+ Rails.logger.warn <<~WARNING
925
+ [E11y] ⚠️ Pipeline Zone Violation Detected!
926
+
927
+ Middleware: #{violation.middleware_class}
928
+ Declared Zone: #{violation.declared_zone}
929
+ Current Zone: #{violation.actual_zone}
930
+
931
+ Problem: This middleware runs after PII filtering but modifies payload fields.
932
+ Risk: PII bypass, security violation, GDPR non-compliance.
933
+
934
+ Fix: Move middleware to pre_processing zone or ensure it only adds non-PII metadata.
935
+
936
+ Documentation: See ADR-015 §3.4 Middleware Zones
937
+ WARNING
938
+
939
+ # In production: Raise error (fail fast!)
940
+ raise violation if Rails.env.production?
941
+ end
942
+ end
943
+ end
944
+ ```
945
+
946
+ ### 3.4.7. Examples: Safe vs Unsafe Middleware
947
+
948
+ **❌ UNSAFE: PII Bypass**
949
+ ```ruby
950
+ class UnsafeMiddleware < E11y::Middleware
951
+ def call(event_data)
952
+ # ❌ BAD: Adds PII after filtering!
953
+ event_data[:payload][:user_email] = Current.user.email
954
+
955
+ @app.call(event_data)
956
+ end
957
+ end
958
+
959
+ # If placed after PIIFiltering middleware:
960
+ # → PII bypass! Email not filtered!
961
+ # → GDPR violation!
962
+ ```
963
+
964
+ **✅ SAFE: Pre-Processing Enrichment**
965
+ ```ruby
966
+ class SafeEnrichmentMiddleware < E11y::Middleware
967
+ middleware_zone :pre_processing
968
+
969
+ def call(event_data)
970
+ # ✅ GOOD: Adds fields BEFORE PII filtering
971
+ event_data[:payload][:request_path] = Current.request.path
972
+ event_data[:payload][:user_agent] = Current.request.user_agent
973
+
974
+ # These fields will be filtered by PIIFiltering middleware if needed
975
+ @app.call(event_data)
976
+ end
977
+ end
978
+ ```
979
+
980
+ **✅ SAFE: Post-Processing Metadata**
981
+ ```ruby
982
+ class SafeMetadataMiddleware < E11y::Middleware
983
+ middleware_zone :post_processing
984
+
985
+ def call(event_data)
986
+ # ✅ GOOD: Adds non-PII metadata AFTER PII filtering
987
+ event_data[:payload][:processing_duration_ms] = calculate_duration(event_data)
988
+ event_data[:payload][:pipeline_version] = E11y::VERSION
989
+
990
+ # ✅ No PII added - safe!
991
+ @app.call(event_data)
992
+ end
993
+ end
994
+ ```
995
+
996
+ ### 3.4.8. Trade-offs & Guidelines (C19)
997
+
998
+ **Trade-offs:**
999
+
1000
+ | Aspect | Pro | Con | Mitigation |
1001
+ |--------|-----|-----|------------|
1002
+ | **Safety** | ✅ Prevents PII bypass | ⚠️ More restrictive API | Clear documentation |
1003
+ | **Flexibility** | ⚠️ Less freedom for custom middleware | ✅ Forces correct patterns | Two zones: pre/post processing |
1004
+ | **Validation** | ✅ Runtime checks catch violations | ⚠️ Adds overhead (~1ms per event) | Only in dev/staging |
1005
+ | **Complexity** | ⚠️ Zones add conceptual overhead | ✅ Clear boundaries | Visual zone diagrams |
1006
+
1007
+ **Guidelines for Custom Middleware:**
1008
+
1009
+ 1. **Pre-Processing Zone (before PII filtering):**
1010
+ - ✅ Add business context fields
1011
+ - ✅ Enrich with database lookups
1012
+ - ✅ Add user attributes (will be filtered if PII)
1013
+ - ❌ Don't assume PII is already filtered
1014
+
1015
+ 2. **Security Zone (PII filtering):**
1016
+ - ❌ DO NOT add custom middleware here
1017
+ - ⚠️ Only E11y::Middleware::PIIFiltering should run
1018
+ - ⚠️ Treat this zone as read-only (no custom code)
1019
+
1020
+ 3. **Post-Processing Zone (after PII filtering):**
1021
+ - ✅ Add technical metadata (timestamps, versions)
1022
+ - ✅ Add tracing context (trace_id, span_id)
1023
+ - ✅ Transform structure (abbreviate keys)
1024
+ - ❌ DO NOT add PII fields
1025
+ - ❌ DO NOT modify filtered fields
1026
+
1027
+ **Monitoring (C19):**
1028
+
1029
+ ```ruby
1030
+ # Prometheus/Yabeda metrics
1031
+ Yabeda.configure do
1032
+ group :e11y_pipeline do
1033
+ counter :zone_violations, comment: 'Pipeline zone violations detected', tags: [:middleware, :zone, :violation_type]
1034
+ counter :pii_bypass_prevented, comment: 'PII bypass attempts prevented', tags: [:middleware]
1035
+ end
1036
+ end
1037
+
1038
+ # Alert rules (Grafana)
1039
+ # Alert: zone_violations > 0 in production (critical!)
1040
+ # Alert: pii_bypass_prevented > 0 (investigate immediately)
1041
+ ```
1042
+
1043
+ **Related Conflicts:**
1044
+ - **C01:** Audit events skip PII filtering (see §3.3)
1045
+ - **C08:** Baggage PII protection (see ADR-007)
1046
+
1047
+ ---
1048
+
1049
+ ## 7. See Also
1050
+
1051
+ - **ADR-001: Architecture** - Pipeline architecture and middleware chain
1052
+ - **ADR-006: Security & Compliance** - PII filtering, encryption requirements
1053
+ - **ADR-012: Event Evolution & Versioning** - Full versioning design
1054
+ - **ADR-013: Reliability & Error Handling** - DLQ replay considerations
1055
+ - **UC-012: Audit Trail** - Audit event use cases
1056
+ - **COMPREHENSIVE-CONFIGURATION.md** - Complete configuration examples
1057
+ - **CONFLICT-ANALYSIS.md** - Complete conflict analysis
1058
+
1059
+ ---
1060
+
1061
+ **Status:** ✅ Stable - Do not change order without updating all ADRs!