e11y 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +69 -0
  4. data/CHANGELOG.md +26 -0
  5. data/CODE_OF_CONDUCT.md +64 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +179 -0
  8. data/Rakefile +37 -0
  9. data/benchmarks/run_all.rb +33 -0
  10. data/config/README.md +83 -0
  11. data/config/loki-local-config.yaml +35 -0
  12. data/config/prometheus.yml +15 -0
  13. data/docker-compose.yml +78 -0
  14. data/docs/00-ICP-AND-TIMELINE.md +483 -0
  15. data/docs/01-SCALE-REQUIREMENTS.md +858 -0
  16. data/docs/ADR-001-architecture.md +2617 -0
  17. data/docs/ADR-002-metrics-yabeda.md +1395 -0
  18. data/docs/ADR-003-slo-observability.md +3337 -0
  19. data/docs/ADR-004-adapter-architecture.md +2385 -0
  20. data/docs/ADR-005-tracing-context.md +1372 -0
  21. data/docs/ADR-006-security-compliance.md +4143 -0
  22. data/docs/ADR-007-opentelemetry-integration.md +1385 -0
  23. data/docs/ADR-008-rails-integration.md +1911 -0
  24. data/docs/ADR-009-cost-optimization.md +2993 -0
  25. data/docs/ADR-010-developer-experience.md +2166 -0
  26. data/docs/ADR-011-testing-strategy.md +1836 -0
  27. data/docs/ADR-012-event-evolution.md +958 -0
  28. data/docs/ADR-013-reliability-error-handling.md +2750 -0
  29. data/docs/ADR-014-event-driven-slo.md +1533 -0
  30. data/docs/ADR-015-middleware-order.md +1061 -0
  31. data/docs/ADR-016-self-monitoring-slo.md +1234 -0
  32. data/docs/API-REFERENCE-L28.md +914 -0
  33. data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
  34. data/docs/IMPLEMENTATION_NOTES.md +2804 -0
  35. data/docs/IMPLEMENTATION_PLAN.md +1971 -0
  36. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
  37. data/docs/PLAN.md +148 -0
  38. data/docs/QUICK-START.md +934 -0
  39. data/docs/README.md +296 -0
  40. data/docs/design/00-memory-optimization.md +593 -0
  41. data/docs/guides/MIGRATION-L27-L28.md +692 -0
  42. data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
  43. data/docs/guides/README.md +44 -0
  44. data/docs/prd/01-overview-vision.md +440 -0
  45. data/docs/use_cases/README.md +119 -0
  46. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
  47. data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
  48. data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
  49. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
  50. data/docs/use_cases/UC-005-sentry-integration.md +759 -0
  51. data/docs/use_cases/UC-006-trace-context-management.md +905 -0
  52. data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
  53. data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
  54. data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
  55. data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
  56. data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
  57. data/docs/use_cases/UC-012-audit-trail.md +2301 -0
  58. data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
  59. data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
  60. data/docs/use_cases/UC-015-cost-optimization.md +735 -0
  61. data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
  62. data/docs/use_cases/UC-017-local-development.md +867 -0
  63. data/docs/use_cases/UC-018-testing-events.md +1081 -0
  64. data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
  65. data/docs/use_cases/UC-020-event-versioning.md +708 -0
  66. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
  67. data/docs/use_cases/UC-022-event-registry.md +648 -0
  68. data/docs/use_cases/backlog.md +226 -0
  69. data/e11y.gemspec +76 -0
  70. data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
  71. data/lib/e11y/adapters/audit_encrypted.rb +239 -0
  72. data/lib/e11y/adapters/base.rb +580 -0
  73. data/lib/e11y/adapters/file.rb +224 -0
  74. data/lib/e11y/adapters/in_memory.rb +216 -0
  75. data/lib/e11y/adapters/loki.rb +333 -0
  76. data/lib/e11y/adapters/otel_logs.rb +203 -0
  77. data/lib/e11y/adapters/registry.rb +141 -0
  78. data/lib/e11y/adapters/sentry.rb +230 -0
  79. data/lib/e11y/adapters/stdout.rb +108 -0
  80. data/lib/e11y/adapters/yabeda.rb +370 -0
  81. data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
  82. data/lib/e11y/buffers/base_buffer.rb +40 -0
  83. data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
  84. data/lib/e11y/buffers/ring_buffer.rb +267 -0
  85. data/lib/e11y/buffers.rb +14 -0
  86. data/lib/e11y/console.rb +122 -0
  87. data/lib/e11y/current.rb +48 -0
  88. data/lib/e11y/event/base.rb +894 -0
  89. data/lib/e11y/event/value_sampling_config.rb +84 -0
  90. data/lib/e11y/events/base_audit_event.rb +43 -0
  91. data/lib/e11y/events/base_payment_event.rb +33 -0
  92. data/lib/e11y/events/rails/cache/delete.rb +21 -0
  93. data/lib/e11y/events/rails/cache/read.rb +23 -0
  94. data/lib/e11y/events/rails/cache/write.rb +22 -0
  95. data/lib/e11y/events/rails/database/query.rb +45 -0
  96. data/lib/e11y/events/rails/http/redirect.rb +21 -0
  97. data/lib/e11y/events/rails/http/request.rb +26 -0
  98. data/lib/e11y/events/rails/http/send_file.rb +21 -0
  99. data/lib/e11y/events/rails/http/start_processing.rb +26 -0
  100. data/lib/e11y/events/rails/job/completed.rb +22 -0
  101. data/lib/e11y/events/rails/job/enqueued.rb +22 -0
  102. data/lib/e11y/events/rails/job/failed.rb +22 -0
  103. data/lib/e11y/events/rails/job/scheduled.rb +23 -0
  104. data/lib/e11y/events/rails/job/started.rb +22 -0
  105. data/lib/e11y/events/rails/log.rb +56 -0
  106. data/lib/e11y/events/rails/view/render.rb +23 -0
  107. data/lib/e11y/events.rb +18 -0
  108. data/lib/e11y/instruments/active_job.rb +201 -0
  109. data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
  110. data/lib/e11y/instruments/sidekiq.rb +175 -0
  111. data/lib/e11y/logger/bridge.rb +205 -0
  112. data/lib/e11y/metrics/cardinality_protection.rb +172 -0
  113. data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
  114. data/lib/e11y/metrics/registry.rb +234 -0
  115. data/lib/e11y/metrics/relabeling.rb +226 -0
  116. data/lib/e11y/metrics.rb +102 -0
  117. data/lib/e11y/middleware/audit_signing.rb +174 -0
  118. data/lib/e11y/middleware/base.rb +140 -0
  119. data/lib/e11y/middleware/event_slo.rb +167 -0
  120. data/lib/e11y/middleware/pii_filter.rb +266 -0
  121. data/lib/e11y/middleware/pii_filtering.rb +280 -0
  122. data/lib/e11y/middleware/rate_limiting.rb +214 -0
  123. data/lib/e11y/middleware/request.rb +163 -0
  124. data/lib/e11y/middleware/routing.rb +157 -0
  125. data/lib/e11y/middleware/sampling.rb +254 -0
  126. data/lib/e11y/middleware/slo.rb +168 -0
  127. data/lib/e11y/middleware/trace_context.rb +131 -0
  128. data/lib/e11y/middleware/validation.rb +118 -0
  129. data/lib/e11y/middleware/versioning.rb +132 -0
  130. data/lib/e11y/middleware.rb +12 -0
  131. data/lib/e11y/pii/patterns.rb +90 -0
  132. data/lib/e11y/pii.rb +13 -0
  133. data/lib/e11y/pipeline/builder.rb +155 -0
  134. data/lib/e11y/pipeline/zone_validator.rb +110 -0
  135. data/lib/e11y/pipeline.rb +12 -0
  136. data/lib/e11y/presets/audit_event.rb +65 -0
  137. data/lib/e11y/presets/debug_event.rb +34 -0
  138. data/lib/e11y/presets/high_value_event.rb +51 -0
  139. data/lib/e11y/presets.rb +19 -0
  140. data/lib/e11y/railtie.rb +138 -0
  141. data/lib/e11y/reliability/circuit_breaker.rb +216 -0
  142. data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
  143. data/lib/e11y/reliability/dlq/filter.rb +117 -0
  144. data/lib/e11y/reliability/retry_handler.rb +207 -0
  145. data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
  146. data/lib/e11y/sampling/error_spike_detector.rb +225 -0
  147. data/lib/e11y/sampling/load_monitor.rb +161 -0
  148. data/lib/e11y/sampling/stratified_tracker.rb +92 -0
  149. data/lib/e11y/sampling/value_extractor.rb +82 -0
  150. data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
  151. data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
  152. data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
  153. data/lib/e11y/slo/event_driven.rb +150 -0
  154. data/lib/e11y/slo/tracker.rb +119 -0
  155. data/lib/e11y/version.rb +9 -0
  156. data/lib/e11y.rb +283 -0
  157. metadata +452 -0
@@ -0,0 +1,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