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