e11y 0.1.0 → 0.2.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +20 -0
  4. data/CHANGELOG.md +151 -13
  5. data/README.md +1138 -104
  6. data/RELEASE.md +254 -0
  7. data/Rakefile +377 -0
  8. data/benchmarks/OPTIMIZATION.md +246 -0
  9. data/benchmarks/README.md +103 -0
  10. data/benchmarks/allocation_profiling.rb +253 -0
  11. data/benchmarks/e11y_benchmarks.rb +447 -0
  12. data/benchmarks/ruby_baseline_allocations.rb +175 -0
  13. data/benchmarks/run_all.rb +9 -21
  14. data/docs/00-ICP-AND-TIMELINE.md +2 -2
  15. data/docs/ADR-001-architecture.md +1 -1
  16. data/docs/ADR-004-adapter-architecture.md +247 -0
  17. data/docs/ADR-009-cost-optimization.md +231 -115
  18. data/docs/ADR-017-multi-rails-compatibility.md +103 -0
  19. data/docs/ADR-INDEX.md +99 -0
  20. data/docs/CONTRIBUTING.md +312 -0
  21. data/docs/IMPLEMENTATION_PLAN.md +1 -1
  22. data/docs/QUICK-START.md +0 -6
  23. data/docs/use_cases/UC-019-retention-based-routing.md +584 -0
  24. data/e11y.gemspec +28 -17
  25. data/lib/e11y/adapters/adaptive_batcher.rb +3 -0
  26. data/lib/e11y/adapters/audit_encrypted.rb +10 -4
  27. data/lib/e11y/adapters/base.rb +15 -0
  28. data/lib/e11y/adapters/file.rb +4 -1
  29. data/lib/e11y/adapters/in_memory.rb +6 -0
  30. data/lib/e11y/adapters/loki.rb +9 -0
  31. data/lib/e11y/adapters/otel_logs.rb +11 -9
  32. data/lib/e11y/adapters/sentry.rb +9 -0
  33. data/lib/e11y/adapters/yabeda.rb +54 -10
  34. data/lib/e11y/buffers.rb +8 -8
  35. data/lib/e11y/console.rb +52 -60
  36. data/lib/e11y/event/base.rb +75 -10
  37. data/lib/e11y/event/value_sampling_config.rb +10 -4
  38. data/lib/e11y/events/rails/http/request.rb +1 -1
  39. data/lib/e11y/instruments/active_job.rb +6 -3
  40. data/lib/e11y/instruments/rails_instrumentation.rb +51 -28
  41. data/lib/e11y/instruments/sidekiq.rb +7 -7
  42. data/lib/e11y/logger/bridge.rb +24 -54
  43. data/lib/e11y/metrics/cardinality_protection.rb +257 -12
  44. data/lib/e11y/metrics/cardinality_tracker.rb +17 -0
  45. data/lib/e11y/metrics/registry.rb +6 -2
  46. data/lib/e11y/metrics/relabeling.rb +0 -56
  47. data/lib/e11y/metrics.rb +6 -1
  48. data/lib/e11y/middleware/audit_signing.rb +12 -9
  49. data/lib/e11y/middleware/pii_filter.rb +18 -10
  50. data/lib/e11y/middleware/request.rb +10 -4
  51. data/lib/e11y/middleware/routing.rb +117 -90
  52. data/lib/e11y/middleware/sampling.rb +47 -28
  53. data/lib/e11y/middleware/trace_context.rb +40 -11
  54. data/lib/e11y/middleware/validation.rb +20 -2
  55. data/lib/e11y/middleware/versioning.rb +1 -1
  56. data/lib/e11y/pii.rb +7 -7
  57. data/lib/e11y/railtie.rb +24 -20
  58. data/lib/e11y/reliability/circuit_breaker.rb +3 -0
  59. data/lib/e11y/reliability/dlq/file_storage.rb +16 -5
  60. data/lib/e11y/reliability/dlq/filter.rb +3 -0
  61. data/lib/e11y/reliability/retry_handler.rb +4 -0
  62. data/lib/e11y/sampling/error_spike_detector.rb +16 -5
  63. data/lib/e11y/sampling/load_monitor.rb +13 -4
  64. data/lib/e11y/self_monitoring/reliability_monitor.rb +3 -0
  65. data/lib/e11y/version.rb +1 -1
  66. data/lib/e11y.rb +86 -9
  67. metadata +83 -38
  68. data/docs/use_cases/UC-019-tiered-storage-migration.md +0 -562
  69. data/lib/e11y/middleware/pii_filtering.rb +0 -280
  70. data/lib/e11y/middleware/slo.rb +0 -168
@@ -2,141 +2,172 @@
2
2
 
3
3
  module E11y
4
4
  module Middleware
5
- # Routing middleware routes events to appropriate buffers/adapters.
5
+ # Routing middleware routes events to appropriate adapters based on retention policies.
6
6
  #
7
7
  # This is the FINAL middleware in the pipeline (adapters zone),
8
8
  # running AFTER all processing (TraceContext, Validation, PII Filtering,
9
9
  # Rate Limiting, Sampling, Versioning).
10
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)
11
+ # **Routing Logic (Priority Order):**
12
+ # 1. **Explicit adapters** - event_data[:adapters] bypasses routing rules
13
+ # 2. **Routing rules** - lambdas from config.routing_rules
14
+ # 3. **Fallback adapters** - config.fallback_adapters if no rule matches
16
15
  #
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)
16
+ # **Routing Rules (Lambdas):**
17
+ # Rules are evaluated in order. Each rule receives event_data hash and returns:
18
+ # - Symbol (adapter name) - route to this adapter
19
+ # - Array<Symbol> (adapter names) - route to multiple adapters
20
+ # - nil - rule doesn't match, try next rule
22
21
  #
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
22
+ # @see ADR-004 §14 (Retention-Based Routing)
23
+ # @see ADR-009 §6 (Cost Optimization via Routing)
24
+ # @see UC-019 (Retention-Based Event Routing)
26
25
  #
27
- # @example Standard event routing
26
+ # @example Explicit adapters (bypass routing)
28
27
  # event_data = {
29
- # event_name: 'Events::OrderPaid',
30
- # severity: :info,
31
- # adapters: [:logs, :errors_tracker],
32
- # payload: { ... }
28
+ # event_name: 'payment.completed',
29
+ # adapters: [:audit_encrypted, :loki], # ← Explicit
30
+ # retention_until: '2027-01-21T...'
33
31
  # }
32
+ # # Routes to: [:audit_encrypted, :loki] (ignores routing rules)
34
33
  #
35
- # # Routes to: main buffer [:logs, :errors_tracker] adapters
36
- #
37
- # @example Debug event routing (request-scoped)
34
+ # @example Audit event routing (via rules)
38
35
  # event_data = {
39
- # event_name: 'Events::DebugInfo',
40
- # severity: :debug,
41
- # adapters: [:logs],
42
- # payload: { ... }
36
+ # event_name: 'user.deleted',
37
+ # audit_event: true,
38
+ # retention_until: '2033-01-21T...'
43
39
  # }
40
+ # # Rule: ->(e) { :audit_encrypted if e[:audit_event] }
41
+ # # Routes to: [:audit_encrypted]
44
42
  #
45
- # # Routes to: request-scoped buffer (buffered, flushed on error)
46
- #
47
- # @example Audit event routing
43
+ # @example Retention-based routing
48
44
  # event_data = {
49
- # event_name: 'Events::PermissionChanged',
50
- # severity: :warn,
51
- # audit_event: true,
52
- # adapters: [:audit_encrypted],
53
- # payload: { ... }
45
+ # event_name: 'order.placed',
46
+ # retention_until: '2026-04-21T...' # 90 days
54
47
  # }
55
- #
56
- # # Routes to: audit buffer → [:audit_encrypted] adapter
48
+ # # Rule: ->(e) { days > 30 ? :s3_standard : :loki }
49
+ # # Routes to: [:s3_standard]
57
50
  class Routing < Base
58
51
  middleware_zone :adapters
59
52
 
60
- # Routes event to appropriate buffer/adapters.
53
+ # Routes event to appropriate adapters based on retention policies.
61
54
  #
62
55
  # @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
56
+ # @option event_data [Array<Symbol>] :adapters Explicit adapter names (optional, bypasses routing)
57
+ # @option event_data [String] :retention_until ISO8601 timestamp (optional, for routing rules)
58
+ # @option event_data [Boolean] :audit_event Audit event flag (optional, for routing rules)
59
+ # @option event_data [Symbol] :severity Event severity (optional, for routing rules)
60
+ # @return [Hash, nil] Event data (passed to next middleware), or nil if dropped
61
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
62
+ # Routing logic requires adapter selection, iteration with error handling,
63
+ # metadata enrichment, and metrics tracking
68
64
  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
65
+ # 1. Determine target adapters (explicit or via routing rules)
66
+ target_adapters = if event_data[:adapters]&.any?
67
+ # Explicit adapters bypass routing rules
68
+ event_data[:adapters]
69
+ else
70
+ # Apply routing rules from configuration
71
+ apply_routing_rules(event_data)
72
+ end
74
73
 
75
- adapters = event_data[:adapters]
76
- severity = event_data[:severity]
77
- audit_event = event_data[:audit_event] || false
74
+ # 2. Write to selected adapters
75
+ target_adapters.each do |adapter_name|
76
+ adapter = E11y.configuration.adapters[adapter_name]
77
+ next unless adapter
78
78
 
79
- # Determine buffer type
80
- buffer_type = determine_buffer_type(severity, audit_event)
79
+ begin
80
+ adapter.write(event_data)
81
+ increment_metric("e11y.middleware.routing.write_success", adapter: adapter_name)
82
+ rescue StandardError => e
83
+ # Log routing error but don't fail pipeline
84
+ warn "E11y routing error for adapter #{adapter_name}: #{e.message}"
85
+ increment_metric("e11y.middleware.routing.write_error", adapter: adapter_name)
86
+ end
87
+ end
81
88
 
82
- # Add routing metadata to event_data
89
+ # 3. Add routing metadata to event_data
83
90
  event_data[:routing] = {
84
- buffer_type: buffer_type,
85
- adapters: adapters,
86
- routed_at: Time.now.utc
91
+ adapters: target_adapters,
92
+ routed_at: Time.now.utc,
93
+ routing_type: event_data[:adapters]&.any? ? :explicit : :rules
87
94
  }
88
95
 
89
- # Increment metrics
96
+ # 4. Increment metrics
90
97
  increment_metric("e11y.middleware.routing.routed",
91
- buffer: buffer_type,
92
- severity: severity,
93
- adapters_count: adapters.size)
98
+ adapters_count: target_adapters.size,
99
+ routing_type: event_data[:routing][:routing_type])
94
100
 
95
- # Log routing decision (for Phase 1 debugging)
96
- log_routing_decision(event_data, buffer_type, adapters) if debug_enabled?
101
+ # 5. Log routing decision (for debugging)
102
+ log_routing_decision(event_data, target_adapters) if debug_enabled?
97
103
 
98
- # Pass to next app (Collector in Phase 2)
99
- @app.call(event_data)
104
+ # 6. Pass to next app (if any)
105
+ @app&.call(event_data)
100
106
  end
101
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
107
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
102
108
 
103
109
  private
104
110
 
105
- # Determine which buffer type to use based on severity and flags.
111
+ # Apply routing rules from configuration.
112
+ #
113
+ # Evaluates routing rules in order until a match is found.
114
+ # Each rule is a lambda that receives event_data and returns:
115
+ # - Symbol (adapter name) - route to this adapter
116
+ # - Array<Symbol> (adapter names) - route to multiple adapters
117
+ # - nil - rule doesn't match, try next rule
118
+ #
119
+ # @param event_data [Hash] Event data with retention_until, audit_event, severity, etc.
120
+ # @return [Array<Symbol>] Target adapter names
106
121
  #
107
- # @param severity [Symbol] Event severity
108
- # @param audit_event [Boolean] Audit event flag
109
- # @return [Symbol] Buffer type (:main, :request_scoped, :audit)
122
+ # @example
123
+ # rules = [
124
+ # ->(event) { :audit_encrypted if event[:audit_event] },
125
+ # ->(event) {
126
+ # days = (Time.parse(event[:retention_until]) - Time.now) / 86400
127
+ # days > 90 ? :s3_glacier : :loki
128
+ # }
129
+ # ]
110
130
  #
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
131
+ # apply_routing_rules(event_data)
132
+ # # => [:audit_encrypted] or [:loki] or [:s3_glacier]
133
+ def apply_routing_rules(event_data)
134
+ matched_adapters = []
118
135
 
119
- :main
136
+ # Apply each rule, collect matched adapters
137
+ rules = E11y.configuration.routing_rules || []
138
+ rules.each do |rule|
139
+ result = rule.call(event_data)
140
+ matched_adapters.concat(Array(result)) if result
141
+ rescue StandardError => e
142
+ # Log rule evaluation error but continue
143
+ warn "E11y routing rule error: #{e.message}"
144
+ end
145
+
146
+ # Return unique adapters or fallback
147
+ if matched_adapters.any?
148
+ matched_adapters.uniq
149
+ else
150
+ E11y.configuration.fallback_adapters || [:stdout]
151
+ end
120
152
  end
121
153
 
122
- # Log routing decision for debugging (Phase 1).
154
+ # Log routing decision for debugging.
123
155
  #
124
156
  # @param event_data [Hash] Event data
125
- # @param buffer_type [Symbol] Determined buffer type
126
157
  # @param adapters [Array<Symbol>] Target adapters
127
158
  # @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}"
159
+ def log_routing_decision(event_data, adapters)
160
+ # TODO: Replace with structured logging
161
+ # Rails.logger.debug "[E11y] Routing: #{event_data[:event_name]} → #{adapters.inspect}"
131
162
  end
132
163
 
133
164
  # Check if debug logging is enabled.
134
165
  #
135
166
  # @return [Boolean]
136
167
  def debug_enabled?
137
- # TODO: Read from configuration in Phase 2
168
+ # TODO: Read from configuration
138
169
  # E11y.configuration.debug_enabled
139
- false # Disabled in Phase 1
170
+ false # Disabled for now
140
171
  end
141
172
 
142
173
  # Placeholder for metrics instrumentation.
@@ -145,12 +176,8 @@ module E11y
145
176
  # @param tags [Hash] Metric tags
146
177
  # @return [void]
147
178
  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
- # )
179
+ # TODO: Integrate with Yabeda/Prometheus
180
+ # Yabeda.e11y.middleware_routing_routed.increment(tags)
154
181
  end
155
182
  end
156
183
  end
@@ -64,33 +64,14 @@ module E11y
64
64
  # @option config [Hash] :error_spike_config ({}) Configuration for ErrorSpikeDetector
65
65
  # @option config [Boolean] :load_based_adaptive (false) Enable load-based adaptive sampling (FEAT-4842)
66
66
  # @option config [Hash] :load_monitor_config ({}) Configuration for LoadMonitor
67
- def initialize(config = {})
68
- # Extract config before calling super (which sets @config)
69
- config ||= {}
70
- @default_sample_rate = config.fetch(:default_sample_rate, 1.0)
71
- @trace_aware = config.fetch(:trace_aware, true)
72
- @severity_rates = config.fetch(:severity_rates, {})
73
- @trace_decisions = {} # Cache for trace-level sampling decisions
74
- @trace_decisions_mutex = Mutex.new
75
-
76
- # Error-based adaptive sampling (FEAT-4838)
77
- @error_based_adaptive = config.fetch(:error_based_adaptive, false)
78
- if @error_based_adaptive
79
- require "e11y/sampling/error_spike_detector"
80
- error_spike_config = config.fetch(:error_spike_config, {})
81
- @error_spike_detector = E11y::Sampling::ErrorSpikeDetector.new(error_spike_config)
82
- end
67
+ def initialize(app = nil, **config)
68
+ # Call parent only if app provided (for production usage)
69
+ super(app) if app
70
+ @app = app
83
71
 
84
- # Load-based adaptive sampling (FEAT-4842)
85
- @load_based_adaptive = config.fetch(:load_based_adaptive, false)
86
- if @load_based_adaptive
87
- require "e11y/sampling/load_monitor"
88
- load_monitor_config = config.fetch(:load_monitor_config, {})
89
- @load_monitor = E11y::Sampling::LoadMonitor.new(load_monitor_config)
90
- end
91
-
92
- # Call super to set @config and other base middleware state
93
- super
72
+ setup_basic_config(config)
73
+ setup_error_based_sampling(config)
74
+ setup_load_based_sampling(config)
94
75
  end
95
76
 
96
77
  # Process event through sampling filter
@@ -133,6 +114,41 @@ module E11y
133
114
 
134
115
  private
135
116
 
117
+ # Setup basic sampling configuration
118
+ #
119
+ # @param config [Hash] Configuration options
120
+ def setup_basic_config(config)
121
+ @default_sample_rate = config.fetch(:default_sample_rate, 1.0)
122
+ @trace_aware = config.fetch(:trace_aware, true)
123
+ @severity_rates = config.fetch(:severity_rates, {})
124
+ @trace_decisions = {} # Cache for trace-level sampling decisions
125
+ @trace_decisions_mutex = Mutex.new
126
+ end
127
+
128
+ # Setup error-based adaptive sampling (FEAT-4838)
129
+ #
130
+ # @param config [Hash] Configuration options
131
+ def setup_error_based_sampling(config)
132
+ @error_based_adaptive = config.fetch(:error_based_adaptive, false)
133
+ return unless @error_based_adaptive
134
+
135
+ require "e11y/sampling/error_spike_detector"
136
+ error_spike_config = config.fetch(:error_spike_config, {})
137
+ @error_spike_detector = E11y::Sampling::ErrorSpikeDetector.new(error_spike_config)
138
+ end
139
+
140
+ # Setup load-based adaptive sampling (FEAT-4842)
141
+ #
142
+ # @param config [Hash] Configuration options
143
+ def setup_load_based_sampling(config)
144
+ @load_based_adaptive = config.fetch(:load_based_adaptive, false)
145
+ return unless @load_based_adaptive
146
+
147
+ require "e11y/sampling/load_monitor"
148
+ load_monitor_config = config.fetch(:load_monitor_config, {})
149
+ @load_monitor = E11y::Sampling::LoadMonitor.new(load_monitor_config)
150
+ end
151
+
136
152
  # Determine if event should be sampled
137
153
  #
138
154
  # @param event_data [Hash] The event payload
@@ -144,8 +160,7 @@ module E11y
144
160
 
145
161
  # 2. Check trace-aware sampling (C05)
146
162
  if @trace_aware && event_data[:trace_id]
147
- return trace_sampling_decision(event_data[:trace_id], event_class,
148
- event_data)
163
+ return trace_sampling_decision(event_data[:trace_id], event_class, event_data)
149
164
  end
150
165
 
151
166
  # 3. Get sample rate for this event
@@ -168,6 +183,9 @@ module E11y
168
183
  # @param event_class [Class] The event class
169
184
  # @param event_data [Hash] Event payload (for value-based sampling)
170
185
  # @return [Float] Sample rate (0.0-1.0)
186
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
187
+ # Sample rate determination follows priority chain: error spike → value-based →
188
+ # load-based → severity → event-level → default
171
189
  def determine_sample_rate(event_class, event_data = nil)
172
190
  # 0. Error-based adaptive sampling (FEAT-4838) - highest priority!
173
191
  if @error_based_adaptive && @error_spike_detector&.error_spike?
@@ -210,6 +228,7 @@ module E11y
210
228
  # 4. Default/load-based rate
211
229
  base_rate
212
230
  end
231
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
213
232
 
214
233
  # Trace-aware sampling decision (C05 Resolution)
215
234
  #
@@ -54,25 +54,54 @@ module E11y
54
54
  # @option event_data [Time,String] :timestamp Existing timestamp (optional)
55
55
  # @return [Hash, nil] Enriched event data, or nil if dropped
56
56
  def call(event_data)
57
- # Add trace_id (propagate from E11y::Current or Thread.current or generate new)
58
- event_data[:trace_id] ||= current_trace_id || generate_trace_id
57
+ enrich_trace_context(event_data)
58
+ enrich_service_context(event_data)
59
+ increment_metric("e11y.middleware.trace_context.processed")
60
+ @app.call(event_data)
61
+ end
59
62
 
60
- # Add span_id (always generate new for this event)
61
- event_data[:span_id] ||= generate_span_id
63
+ private
62
64
 
63
- # Add parent_trace_id (if job has parent trace) - C17 Resolution
65
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
66
+ # Add distributed tracing fields to event data
67
+ # @param event_data [Hash] Event data to enrich
68
+ # @return [void]
69
+ def enrich_trace_context(event_data)
70
+ event_data[:trace_id] ||= current_trace_id || generate_trace_id
71
+ event_data[:span_id] ||= generate_span_id
64
72
  event_data[:parent_trace_id] ||= current_parent_trace_id if current_parent_trace_id
65
73
 
66
- # Add timestamp (use existing or current time)
67
- event_data[:timestamp] ||= format_timestamp(Time.now.utc)
74
+ # Format timestamp if it's a Time object
75
+ timestamp = event_data[:timestamp]
76
+ event_data[:timestamp] = if timestamp.is_a?(Time)
77
+ format_timestamp(timestamp)
78
+ else
79
+ timestamp || format_timestamp(Time.now.utc)
80
+ end
68
81
 
69
- # Increment metrics
70
- increment_metric("e11y.middleware.trace_context.processed")
82
+ # Calculate retention_until from retention_period
83
+ if event_data[:retention_period] && !event_data[:retention_until]
84
+ # Parse timestamp back to Time to calculate retention_until
85
+ base_time = timestamp.is_a?(Time) ? timestamp : Time.parse(event_data[:timestamp])
86
+ retention_time = base_time + event_data[:retention_period]
87
+ event_data[:retention_until] = retention_time.iso8601
88
+ end
71
89
 
72
- @app.call(event_data)
90
+ # Add audit_event flag
91
+ event_class = event_data[:event_class]
92
+ return unless event_class.respond_to?(:audit_event?)
93
+
94
+ event_data[:audit_event] = event_class.audit_event?
73
95
  end
96
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
74
97
 
75
- private
98
+ # Add service context fields to event data
99
+ # @param event_data [Hash] Event data to enrich
100
+ # @return [void]
101
+ def enrich_service_context(event_data)
102
+ event_data[:service_name] ||= E11y.config.service_name
103
+ event_data[:environment] ||= E11y.config.environment
104
+ end
76
105
 
77
106
  # Get current trace ID from E11y::Current or thread-local storage (request context).
78
107
  #
@@ -56,7 +56,7 @@ module E11y
56
56
  # @option event_data [Hash] :payload The event payload (required)
57
57
  # @return [Hash, nil] Validated event data, or nil if dropped
58
58
  # @raise [E11y::ValidationError] if validation fails
59
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
59
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
60
60
  def call(event_data)
61
61
  # Skip validation if no event_class or payload
62
62
  return @app.call(event_data) unless event_data[:event_class] && event_data[:payload]
@@ -64,6 +64,24 @@ module E11y
64
64
  event_class = event_data[:event_class]
65
65
  payload = event_data[:payload]
66
66
 
67
+ # Check validation mode (FEAT-4850: performance tuning)
68
+ validation_mode = event_class.respond_to?(:validation_mode) ? event_class.validation_mode : :always
69
+
70
+ # Skip validation if mode is :never
71
+ if validation_mode == :never
72
+ increment_metric("e11y.middleware.validation.skipped")
73
+ return @app.call(event_data)
74
+ end
75
+
76
+ # Skip validation probabilistically if mode is :sampled
77
+ if validation_mode == :sampled
78
+ sample_rate = event_class.respond_to?(:validation_sample_rate) ? event_class.validation_sample_rate : 0.01
79
+ if rand >= sample_rate
80
+ increment_metric("e11y.middleware.validation.skipped")
81
+ return @app.call(event_data)
82
+ end
83
+ end
84
+
67
85
  # Get compiled schema from event class
68
86
  schema = event_class.compiled_schema
69
87
 
@@ -88,7 +106,7 @@ module E11y
88
106
  raise E11y::ValidationError, error_message
89
107
  end
90
108
  end
91
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
109
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
92
110
 
93
111
  private
94
112
 
@@ -125,7 +125,7 @@ module E11y
125
125
  name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') # ABCWord → ABC_Word
126
126
  .gsub(/([a-z\d])([A-Z])/, '\1_\2') # wordWord → word_Word
127
127
  .downcase
128
- .gsub("_", ".") # Convert underscores to dots for event names
128
+ .tr("_", ".") # Convert underscores to dots for event names
129
129
  end
130
130
  end
131
131
  end
data/lib/e11y/pii.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # E11y::PII module - PII detection and filtering utilities
4
- #
5
- # This module provides PII pattern detection and filtering strategies
6
- # for the PIIFiltering middleware.
7
- #
8
- # @see E11y::PII::Patterns
9
- # @see E11y::Middleware::PIIFiltering
10
3
  module E11y
4
+ # PII detection and filtering utilities
5
+ #
6
+ # This module provides PII pattern detection and filtering strategies
7
+ # for the PIIFiltering middleware.
8
+ #
9
+ # @see E11y::PII::Patterns
10
+ # @see E11y::Middleware::PIIFiltering
11
11
  module PII
12
12
  end
13
13
  end
data/lib/e11y/railtie.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Skip Railtie if Rails is not available
4
- return unless defined?(Rails)
5
-
6
3
  require "rails/railtie"
7
4
 
8
5
  module E11y
@@ -29,26 +26,41 @@ module E11y
29
26
  # end
30
27
  #
31
28
  # @see ADR-008 §3 (Railtie & Initialization)
29
+
30
+ # Rails integration engine that handles E11y initialization and setup
31
+ #
32
+ # This Railtie manages the lifecycle of E11y within a Rails application,
33
+ # including configuration, middleware insertion, instrumentation setup,
34
+ # and console integration.
32
35
  class Railtie < Rails::Railtie
36
+ # Derive service name from Rails application class
37
+ # @return [String] Service name (e.g., "my_app")
38
+ def self.derive_service_name
39
+ Rails.application.class.module_parent_name.underscore
40
+ rescue StandardError
41
+ "rails_app"
42
+ end
43
+
33
44
  # Run before framework initialization
34
45
  config.before_initialize do
35
46
  # Set up basic configuration from Rails
36
47
  E11y.configure do |config|
37
- config.environment = Rails.env.to_s
38
- config.service_name = derive_service_name
39
- config.enabled = !Rails.env.test? # Disabled in tests by default
48
+ config.environment ||= Rails.env.to_s
49
+ config.service_name ||= E11y::Railtie.derive_service_name
50
+ # Only set enabled if not already configured
51
+ config.enabled = !Rails.env.test? if config.enabled.nil?
40
52
  end
41
53
  end
42
54
 
43
- # Run after framework initialization
44
- config.after_initialize do
55
+ # Setup instrumentation after Rails initialization
56
+ initializer "e11y.setup_instrumentation", after: :load_config_initializers do
45
57
  next unless E11y.config.enabled
46
58
 
47
59
  # Setup instruments (each can be enabled/disabled separately)
48
- setup_rails_instrumentation if E11y.config.rails_instrumentation&.enabled
49
- setup_logger_bridge if E11y.config.logger_bridge&.enabled
50
- setup_sidekiq if defined?(::Sidekiq) && E11y.config.sidekiq&.enabled
51
- setup_active_job if defined?(::ActiveJob) && E11y.config.active_job&.enabled
60
+ E11y::Railtie.setup_rails_instrumentation if E11y.config.rails_instrumentation&.enabled
61
+ E11y::Railtie.setup_logger_bridge if E11y.config.logger_bridge&.enabled
62
+ E11y::Railtie.setup_sidekiq if defined?(::Sidekiq) && E11y.config.sidekiq&.enabled
63
+ E11y::Railtie.setup_active_job if defined?(::ActiveJob) && E11y.config.active_job&.enabled
52
64
  end
53
65
 
54
66
  # Middleware insertion
@@ -81,14 +93,6 @@ module E11y
81
93
  # load 'e11y/tasks.rake'
82
94
  end
83
95
 
84
- # Derive service name from Rails application class
85
- # @return [String] Service name (e.g., "my_app")
86
- def self.derive_service_name
87
- Rails.application.class.module_parent_name.underscore
88
- rescue StandardError
89
- "rails_app"
90
- end
91
-
92
96
  # Setup Rails instrumentation (ActiveSupport::Notifications → E11y)
93
97
  # @return [void]
94
98
  def self.setup_rails_instrumentation
@@ -17,6 +17,8 @@ module E11y
17
17
  #
18
18
  # @see ADR-013 §5 (Circuit Breaker)
19
19
  # @see UC-021 §4 (Circuit Breaker for Adapters)
20
+ # rubocop:disable Metrics/ClassLength
21
+ # Circuit breaker is a cohesive state machine with complex state transitions and recovery logic
20
22
  class CircuitBreaker
21
23
  # Circuit is closed (healthy) - all requests pass through
22
24
  STATE_CLOSED = :closed
@@ -212,5 +214,6 @@ module E11y
212
214
  # E11y::Metrics.increment(metric_name, tags.merge(adapter: @adapter_name, state: @state))
213
215
  end
214
216
  end
217
+ # rubocop:enable Metrics/ClassLength
215
218
  end
216
219
  end