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,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# Rate Limiting Middleware (UC-011, C02 Resolution)
|
|
6
|
+
#
|
|
7
|
+
# Protects adapters from event floods using token bucket algorithm.
|
|
8
|
+
# Critical events bypass rate limiting and go to DLQ (C02 Resolution).
|
|
9
|
+
#
|
|
10
|
+
# **Features:**
|
|
11
|
+
# - Global rate limit (e.g., 10K events/sec)
|
|
12
|
+
# - Per-event type rate limit (e.g., 1K payment.retry/sec)
|
|
13
|
+
# - In-memory token bucket (no Redis dependency)
|
|
14
|
+
# - Critical events bypass (C02 Resolution)
|
|
15
|
+
# - DLQ integration for rate-limited critical events
|
|
16
|
+
#
|
|
17
|
+
# **ADR References:**
|
|
18
|
+
# - ADR-013 §4.6 (C02 Resolution - Rate Limiting × DLQ Filter)
|
|
19
|
+
# - ADR-015 §3 (Middleware Order - RateLimiting in :routing zone)
|
|
20
|
+
#
|
|
21
|
+
# **Use Case:** UC-011 (Rate Limiting - DoS Protection)
|
|
22
|
+
#
|
|
23
|
+
# @example Configuration
|
|
24
|
+
# E11y.configure do |config|
|
|
25
|
+
# config.pipeline.use E11y::Middleware::RateLimiting,
|
|
26
|
+
# global_limit: 10_000, # Max 10K events/sec globally
|
|
27
|
+
# per_event_limit: 1_000, # Max 1K events/sec per event type
|
|
28
|
+
# window: 1.0 # 1 second window
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# @example Critical Event Bypass (C02)
|
|
32
|
+
# # Payment events bypass rate limiting → DLQ if limited
|
|
33
|
+
# config.dlq_filter.always_save_patterns = [/^payment\./]
|
|
34
|
+
#
|
|
35
|
+
# # Result: Rate-limited payment events go to DLQ, not dropped
|
|
36
|
+
#
|
|
37
|
+
# @see ADR-013 §4.6 for C02 Resolution details
|
|
38
|
+
# @see UC-011 for rate limiting use cases
|
|
39
|
+
class RateLimiting < Base
|
|
40
|
+
# Initialize rate limiting middleware
|
|
41
|
+
#
|
|
42
|
+
# @param app [Object] Next middleware in pipeline
|
|
43
|
+
# @param global_limit [Integer] Max events/sec globally (default: 10_000)
|
|
44
|
+
# @param per_event_limit [Integer] Max events/sec per event type (default: 1_000)
|
|
45
|
+
# @param window [Float] Time window in seconds (default: 1.0)
|
|
46
|
+
def initialize(app, global_limit: 10_000, per_event_limit: 1_000, window: 1.0)
|
|
47
|
+
super(app)
|
|
48
|
+
@global_limit = global_limit
|
|
49
|
+
@per_event_limit = per_event_limit
|
|
50
|
+
@window = window
|
|
51
|
+
|
|
52
|
+
# Token buckets for rate limiting
|
|
53
|
+
@global_bucket = TokenBucket.new(capacity: @global_limit, refill_rate: @global_limit, window: @window)
|
|
54
|
+
@per_event_buckets = Hash.new do |hash, event_name|
|
|
55
|
+
hash[event_name] = TokenBucket.new(
|
|
56
|
+
capacity: @per_event_limit,
|
|
57
|
+
refill_rate: @per_event_limit,
|
|
58
|
+
window: @window
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@mutex = Mutex.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Process event through rate limiting
|
|
66
|
+
#
|
|
67
|
+
# @param event_data [Hash] Event payload
|
|
68
|
+
# @return [Hash, nil] Event data if allowed, nil if rate limited
|
|
69
|
+
def call(event_data)
|
|
70
|
+
event_name = event_data[:event_name]
|
|
71
|
+
|
|
72
|
+
# Check global rate limit
|
|
73
|
+
unless @global_bucket.allow?
|
|
74
|
+
handle_rate_limited(event_data, :global)
|
|
75
|
+
return nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check per-event rate limit
|
|
79
|
+
per_event_bucket = @mutex.synchronize { @per_event_buckets[event_name] }
|
|
80
|
+
unless per_event_bucket.allow?
|
|
81
|
+
handle_rate_limited(event_data, :per_event)
|
|
82
|
+
return nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Rate limit not exceeded - continue pipeline
|
|
86
|
+
event_data
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# Handle rate-limited event (C02 Resolution)
|
|
92
|
+
#
|
|
93
|
+
# Critical events are saved to DLQ, non-critical events are dropped.
|
|
94
|
+
#
|
|
95
|
+
# @param event_data [Hash] Event payload
|
|
96
|
+
# @param limit_type [Symbol] :global or :per_event
|
|
97
|
+
def handle_rate_limited(event_data, limit_type)
|
|
98
|
+
event_name = event_data[:event_name]
|
|
99
|
+
|
|
100
|
+
# Log rate limiting
|
|
101
|
+
warn "[E11y] Rate limit exceeded (#{limit_type}) for event: #{event_name}"
|
|
102
|
+
|
|
103
|
+
# C02 Resolution: Check if event should be saved to DLQ
|
|
104
|
+
return unless should_save_to_dlq?(event_data)
|
|
105
|
+
|
|
106
|
+
save_to_dlq(event_data, limit_type)
|
|
107
|
+
|
|
108
|
+
# Non-critical events are dropped (no DLQ)
|
|
109
|
+
# TODO: Track metric e11y.rate_limiter.dropped
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check if rate-limited event should be saved to DLQ (C02 Resolution)
|
|
113
|
+
#
|
|
114
|
+
# @param event_data [Hash] Event payload
|
|
115
|
+
# @return [Boolean] true if event should be saved to DLQ
|
|
116
|
+
def should_save_to_dlq?(event_data)
|
|
117
|
+
return false unless E11y.config.respond_to?(:dlq_filter)
|
|
118
|
+
|
|
119
|
+
# Use DLQ filter to determine if event is critical
|
|
120
|
+
dlq_filter = E11y.config.dlq_filter
|
|
121
|
+
return false unless dlq_filter
|
|
122
|
+
|
|
123
|
+
# Check if event matches always_save_patterns
|
|
124
|
+
event_name = event_data[:event_name]
|
|
125
|
+
dlq_filter.always_save_patterns&.any? { |pattern| pattern.match?(event_name) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Save rate-limited critical event to DLQ (C02 Resolution)
|
|
129
|
+
#
|
|
130
|
+
# @param event_data [Hash] Event payload
|
|
131
|
+
# @param limit_type [Symbol] :global or :per_event
|
|
132
|
+
def save_to_dlq(event_data, limit_type)
|
|
133
|
+
return unless E11y.config.respond_to?(:dlq_storage)
|
|
134
|
+
|
|
135
|
+
dlq_storage = E11y.config.dlq_storage
|
|
136
|
+
return unless dlq_storage
|
|
137
|
+
|
|
138
|
+
dlq_storage.save(event_data, metadata: {
|
|
139
|
+
reason: "rate_limited_#{limit_type}",
|
|
140
|
+
limit_type: limit_type,
|
|
141
|
+
global_limit: @global_limit,
|
|
142
|
+
per_event_limit: @per_event_limit,
|
|
143
|
+
timestamp: Time.now.utc.iso8601
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
warn "[E11y] Rate-limited critical event saved to DLQ: #{event_data[:event_name]}"
|
|
147
|
+
# TODO: Track metric e11y.rate_limiter.dlq_saved
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
# Don't fail if DLQ save fails (C18 Resolution)
|
|
150
|
+
warn "[E11y] Failed to save rate-limited event to DLQ: #{e.message}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Token Bucket implementation for rate limiting
|
|
154
|
+
#
|
|
155
|
+
# Thread-safe token bucket algorithm for rate limiting.
|
|
156
|
+
#
|
|
157
|
+
# @see https://en.wikipedia.org/wiki/Token_bucket
|
|
158
|
+
class TokenBucket
|
|
159
|
+
# Initialize token bucket
|
|
160
|
+
#
|
|
161
|
+
# @param capacity [Integer] Maximum tokens in bucket
|
|
162
|
+
# @param refill_rate [Float] Tokens added per second
|
|
163
|
+
# @param window [Float] Time window in seconds
|
|
164
|
+
def initialize(capacity:, refill_rate:, window:)
|
|
165
|
+
@capacity = capacity
|
|
166
|
+
@refill_rate = refill_rate
|
|
167
|
+
@window = window
|
|
168
|
+
@tokens = capacity.to_f
|
|
169
|
+
@last_refill = Time.now
|
|
170
|
+
@mutex = Mutex.new
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check if request is allowed (consumes 1 token if available)
|
|
174
|
+
#
|
|
175
|
+
# @return [Boolean] true if request allowed
|
|
176
|
+
def allow?
|
|
177
|
+
@mutex.synchronize do
|
|
178
|
+
refill_tokens
|
|
179
|
+
if @tokens >= 1.0
|
|
180
|
+
@tokens -= 1.0
|
|
181
|
+
true
|
|
182
|
+
else
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Current token count (for debugging)
|
|
189
|
+
#
|
|
190
|
+
# @return [Float] Current tokens available
|
|
191
|
+
def tokens
|
|
192
|
+
@mutex.synchronize do
|
|
193
|
+
refill_tokens
|
|
194
|
+
@tokens
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
# Refill tokens based on elapsed time
|
|
201
|
+
def refill_tokens
|
|
202
|
+
now = Time.now
|
|
203
|
+
elapsed = now - @last_refill
|
|
204
|
+
return if elapsed <= 0
|
|
205
|
+
|
|
206
|
+
# Calculate tokens to add (refill_rate tokens per second)
|
|
207
|
+
tokens_to_add = elapsed * @refill_rate
|
|
208
|
+
@tokens = [@tokens + tokens_to_add, @capacity].min
|
|
209
|
+
@last_refill = now
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/request"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Middleware
|
|
8
|
+
# Request Middleware for Rails/Rack applications
|
|
9
|
+
#
|
|
10
|
+
# Provides request-scoped context and trace propagation:
|
|
11
|
+
# - Extracts or generates trace_id
|
|
12
|
+
# - Sets up request context (E11y::Current)
|
|
13
|
+
# - Manages request-scoped buffer (optional)
|
|
14
|
+
# - Tracks HTTP request lifecycle
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage
|
|
17
|
+
# # Automatically inserted by E11y::Railtie
|
|
18
|
+
# # app.middleware.insert_before(Rails::Rack::Logger, E11y::Middleware::Request)
|
|
19
|
+
#
|
|
20
|
+
# @example Manual usage (non-Rails)
|
|
21
|
+
# use E11y::Middleware::Request
|
|
22
|
+
#
|
|
23
|
+
# @see ADR-008 §8.1 (Request Middleware)
|
|
24
|
+
# @see ADR-005 §3 (Trace Context Management)
|
|
25
|
+
class Request
|
|
26
|
+
# Initialize middleware
|
|
27
|
+
# @param app [Object] Rack app
|
|
28
|
+
def initialize(app)
|
|
29
|
+
@app = app
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Process request
|
|
33
|
+
# @param env [Hash] Rack environment
|
|
34
|
+
# @return [Array] Rack response [status, headers, body]
|
|
35
|
+
def call(env)
|
|
36
|
+
request = Rack::Request.new(env)
|
|
37
|
+
|
|
38
|
+
# Extract or generate trace_id
|
|
39
|
+
trace_id = extract_trace_id(request) || generate_trace_id
|
|
40
|
+
span_id = generate_span_id
|
|
41
|
+
|
|
42
|
+
# Set request context (ActiveSupport::CurrentAttributes)
|
|
43
|
+
E11y::Current.trace_id = trace_id
|
|
44
|
+
E11y::Current.span_id = span_id
|
|
45
|
+
E11y::Current.request_id = request_id(env)
|
|
46
|
+
E11y::Current.user_id = extract_user_id(env)
|
|
47
|
+
E11y::Current.ip_address = request.ip
|
|
48
|
+
E11y::Current.user_agent = request.user_agent
|
|
49
|
+
E11y::Current.request_method = request.request_method
|
|
50
|
+
E11y::Current.request_path = request.path
|
|
51
|
+
|
|
52
|
+
# Start request-scoped buffer (for debug events)
|
|
53
|
+
E11y::Buffers::RequestScopedBuffer.start! if E11y.config.request_buffer&.enabled
|
|
54
|
+
|
|
55
|
+
# Track request start time for SLO
|
|
56
|
+
start_time = Time.now
|
|
57
|
+
|
|
58
|
+
# Call next middleware/app
|
|
59
|
+
status, headers, body = @app.call(env)
|
|
60
|
+
|
|
61
|
+
# Track SLO metrics (if enabled)
|
|
62
|
+
track_http_request_slo(env, status, start_time)
|
|
63
|
+
|
|
64
|
+
# Add trace headers to response
|
|
65
|
+
headers["X-E11y-Trace-Id"] = trace_id
|
|
66
|
+
headers["X-E11y-Span-Id"] = span_id
|
|
67
|
+
|
|
68
|
+
[status, headers, body]
|
|
69
|
+
rescue StandardError
|
|
70
|
+
# Flush request buffer on error (includes debug events)
|
|
71
|
+
E11y::Buffers::RequestScopedBuffer.flush_on_error! if E11y.config.request_buffer&.enabled
|
|
72
|
+
|
|
73
|
+
raise # Re-raise original exception
|
|
74
|
+
ensure
|
|
75
|
+
# Flush request buffer on success (not on error, already flushed above)
|
|
76
|
+
# We need to check if we're here from normal completion or exception
|
|
77
|
+
# If there was an exception, buffer was already flushed in rescue block
|
|
78
|
+
if !$ERROR_INFO && E11y.config.request_buffer&.enabled # No exception occurred
|
|
79
|
+
E11y::Buffers::RequestScopedBuffer.flush!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Reset context
|
|
83
|
+
E11y::Current.reset
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Extract trace_id from request headers (W3C Trace Context or custom headers)
|
|
89
|
+
# @param request [Rack::Request] Rack request
|
|
90
|
+
# @return [String, nil] Trace ID or nil if not found
|
|
91
|
+
def extract_trace_id(request)
|
|
92
|
+
# W3C Trace Context (traceparent header)
|
|
93
|
+
# Format: version-trace_id-span_id-flags
|
|
94
|
+
# Example: 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01
|
|
95
|
+
traceparent = request.get_header("HTTP_TRACEPARENT")
|
|
96
|
+
return traceparent.split("-")[1] if traceparent
|
|
97
|
+
|
|
98
|
+
# X-Request-ID (Rails default)
|
|
99
|
+
request.get_header("HTTP_X_REQUEST_ID") ||
|
|
100
|
+
# X-Trace-Id (custom)
|
|
101
|
+
request.get_header("HTTP_X_TRACE_ID")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Extract request_id from Rack env
|
|
105
|
+
# @param env [Hash] Rack environment
|
|
106
|
+
# @return [String] Request ID
|
|
107
|
+
def request_id(env)
|
|
108
|
+
env["action_dispatch.request_id"] || generate_trace_id
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Generate new trace_id
|
|
112
|
+
# @return [String] 32-character hex trace ID
|
|
113
|
+
def generate_trace_id
|
|
114
|
+
SecureRandom.hex(16)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Generate new span_id
|
|
118
|
+
# @return [String] 16-character hex span ID
|
|
119
|
+
def generate_span_id
|
|
120
|
+
SecureRandom.hex(8)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Extract user_id from Rack env (Warden, Devise, or session)
|
|
124
|
+
# @param env [Hash] Rack environment
|
|
125
|
+
# @return [Integer, String, nil] User ID if available
|
|
126
|
+
def extract_user_id(env)
|
|
127
|
+
# Warden (Devise)
|
|
128
|
+
return env["warden"]&.user&.id if env["warden"]
|
|
129
|
+
|
|
130
|
+
# Rack session
|
|
131
|
+
env["rack.session"]&.[]("user_id")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Track HTTP request for SLO metrics (if enabled).
|
|
135
|
+
#
|
|
136
|
+
# @param env [Hash] Rack environment
|
|
137
|
+
# @param status [Integer] HTTP status code
|
|
138
|
+
# @param start_time [Time] Request start time
|
|
139
|
+
# @return [void]
|
|
140
|
+
# @api private
|
|
141
|
+
def track_http_request_slo(env, status, start_time)
|
|
142
|
+
return unless E11y.config.slo_tracking&.enabled
|
|
143
|
+
|
|
144
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
145
|
+
|
|
146
|
+
# Extract controller and action from Rails routing
|
|
147
|
+
controller = env["action_controller.instance"]&.controller_name || "unknown"
|
|
148
|
+
action = env["action_controller.instance"]&.action_name || "unknown"
|
|
149
|
+
|
|
150
|
+
require "e11y/slo/tracker"
|
|
151
|
+
E11y::SLO::Tracker.track_http_request(
|
|
152
|
+
controller: controller,
|
|
153
|
+
action: action,
|
|
154
|
+
status: status,
|
|
155
|
+
duration_ms: duration_ms
|
|
156
|
+
)
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
# Don't fail if SLO tracking fails
|
|
159
|
+
warn "[E11y] SLO tracking error: #{e.message}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# Routing middleware routes events to appropriate buffers/adapters.
|
|
6
|
+
#
|
|
7
|
+
# This is the FINAL middleware in the pipeline (adapters zone),
|
|
8
|
+
# running AFTER all processing (TraceContext, Validation, PII Filtering,
|
|
9
|
+
# Rate Limiting, Sampling, Versioning).
|
|
10
|
+
#
|
|
11
|
+
# **Phase 1 Implementation:**
|
|
12
|
+
# - Determines target adapters from event configuration
|
|
13
|
+
# - Logs routing decisions for debugging
|
|
14
|
+
# - Increments metrics for observability
|
|
15
|
+
# - Does NOT actually send events (Phase 2: Collector will handle delivery)
|
|
16
|
+
#
|
|
17
|
+
# **Routing Logic:**
|
|
18
|
+
# 1. Get adapters from event_data[:adapters] (configured in Event class)
|
|
19
|
+
# 2. Determine buffer type based on severity/flags
|
|
20
|
+
# 3. Log/metric routing decision
|
|
21
|
+
# 4. Pass event_data to next app (Collector in Phase 2)
|
|
22
|
+
#
|
|
23
|
+
# @see ADR-015 §3.1 Pipeline Flow (line 113-117)
|
|
24
|
+
# @see ADR-001 §3 Adapter Architecture
|
|
25
|
+
# @see UC-001 Request-Scoped Debug Buffering
|
|
26
|
+
#
|
|
27
|
+
# @example Standard event routing
|
|
28
|
+
# event_data = {
|
|
29
|
+
# event_name: 'Events::OrderPaid',
|
|
30
|
+
# severity: :info,
|
|
31
|
+
# adapters: [:logs, :errors_tracker],
|
|
32
|
+
# payload: { ... }
|
|
33
|
+
# }
|
|
34
|
+
#
|
|
35
|
+
# # Routes to: main buffer → [:logs, :errors_tracker] adapters
|
|
36
|
+
#
|
|
37
|
+
# @example Debug event routing (request-scoped)
|
|
38
|
+
# event_data = {
|
|
39
|
+
# event_name: 'Events::DebugInfo',
|
|
40
|
+
# severity: :debug,
|
|
41
|
+
# adapters: [:logs],
|
|
42
|
+
# payload: { ... }
|
|
43
|
+
# }
|
|
44
|
+
#
|
|
45
|
+
# # Routes to: request-scoped buffer (buffered, flushed on error)
|
|
46
|
+
#
|
|
47
|
+
# @example Audit event routing
|
|
48
|
+
# event_data = {
|
|
49
|
+
# event_name: 'Events::PermissionChanged',
|
|
50
|
+
# severity: :warn,
|
|
51
|
+
# audit_event: true,
|
|
52
|
+
# adapters: [:audit_encrypted],
|
|
53
|
+
# payload: { ... }
|
|
54
|
+
# }
|
|
55
|
+
#
|
|
56
|
+
# # Routes to: audit buffer → [:audit_encrypted] adapter
|
|
57
|
+
class Routing < Base
|
|
58
|
+
middleware_zone :adapters
|
|
59
|
+
|
|
60
|
+
# Routes event to appropriate buffer/adapters.
|
|
61
|
+
#
|
|
62
|
+
# @param event_data [Hash] The event data to route
|
|
63
|
+
# @option event_data [Array<Symbol>] :adapters Target adapter names (required)
|
|
64
|
+
# @option event_data [Symbol] :severity Event severity (required)
|
|
65
|
+
# @option event_data [Boolean] :audit_event Audit event flag (optional)
|
|
66
|
+
# @return [Hash, nil] Event data (passed to Collector in Phase 2), or nil if dropped
|
|
67
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
68
|
+
def call(event_data)
|
|
69
|
+
# Skip if no adapters or severity
|
|
70
|
+
unless event_data[:adapters] && event_data[:severity]
|
|
71
|
+
increment_metric("e11y.middleware.routing.skipped")
|
|
72
|
+
return @app.call(event_data)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
adapters = event_data[:adapters]
|
|
76
|
+
severity = event_data[:severity]
|
|
77
|
+
audit_event = event_data[:audit_event] || false
|
|
78
|
+
|
|
79
|
+
# Determine buffer type
|
|
80
|
+
buffer_type = determine_buffer_type(severity, audit_event)
|
|
81
|
+
|
|
82
|
+
# Add routing metadata to event_data
|
|
83
|
+
event_data[:routing] = {
|
|
84
|
+
buffer_type: buffer_type,
|
|
85
|
+
adapters: adapters,
|
|
86
|
+
routed_at: Time.now.utc
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Increment metrics
|
|
90
|
+
increment_metric("e11y.middleware.routing.routed",
|
|
91
|
+
buffer: buffer_type,
|
|
92
|
+
severity: severity,
|
|
93
|
+
adapters_count: adapters.size)
|
|
94
|
+
|
|
95
|
+
# Log routing decision (for Phase 1 debugging)
|
|
96
|
+
log_routing_decision(event_data, buffer_type, adapters) if debug_enabled?
|
|
97
|
+
|
|
98
|
+
# Pass to next app (Collector in Phase 2)
|
|
99
|
+
@app.call(event_data)
|
|
100
|
+
end
|
|
101
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Determine which buffer type to use based on severity and flags.
|
|
106
|
+
#
|
|
107
|
+
# @param severity [Symbol] Event severity
|
|
108
|
+
# @param audit_event [Boolean] Audit event flag
|
|
109
|
+
# @return [Symbol] Buffer type (:main, :request_scoped, :audit)
|
|
110
|
+
#
|
|
111
|
+
# Routing rules:
|
|
112
|
+
# - Audit events → :audit buffer (separate pipeline, no PII filtering)
|
|
113
|
+
# - Debug events → :request_scoped buffer (buffered, flushed on error)
|
|
114
|
+
# - Other events → :main buffer (immediate sending)
|
|
115
|
+
def determine_buffer_type(severity, audit_event)
|
|
116
|
+
return :audit if audit_event
|
|
117
|
+
return :request_scoped if severity == :debug
|
|
118
|
+
|
|
119
|
+
:main
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Log routing decision for debugging (Phase 1).
|
|
123
|
+
#
|
|
124
|
+
# @param event_data [Hash] Event data
|
|
125
|
+
# @param buffer_type [Symbol] Determined buffer type
|
|
126
|
+
# @param adapters [Array<Symbol>] Target adapters
|
|
127
|
+
# @return [void]
|
|
128
|
+
def log_routing_decision(event_data, buffer_type, adapters)
|
|
129
|
+
# TODO: Replace with structured logging in Phase 2
|
|
130
|
+
# Rails.logger.debug "[E11y] Routing: #{event_data[:event_name]} → #{buffer_type} → #{adapters.inspect}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Check if debug logging is enabled.
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean]
|
|
136
|
+
def debug_enabled?
|
|
137
|
+
# TODO: Read from configuration in Phase 2
|
|
138
|
+
# E11y.configuration.debug_enabled
|
|
139
|
+
false # Disabled in Phase 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Placeholder for metrics instrumentation.
|
|
143
|
+
#
|
|
144
|
+
# @param metric_name [String] Metric name
|
|
145
|
+
# @param tags [Hash] Metric tags
|
|
146
|
+
# @return [void]
|
|
147
|
+
def increment_metric(_metric_name, **_tags)
|
|
148
|
+
# TODO: Integrate with Yabeda/Prometheus in Phase 2
|
|
149
|
+
# Yabeda.e11y.middleware_routing_routed.increment(
|
|
150
|
+
# buffer: buffer,
|
|
151
|
+
# severity: severity,
|
|
152
|
+
# adapters_count: adapters_count
|
|
153
|
+
# )
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|