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