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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +151 -13
- data/README.md +1138 -104
- data/RELEASE.md +254 -0
- data/Rakefile +377 -0
- data/benchmarks/OPTIMIZATION.md +246 -0
- data/benchmarks/README.md +103 -0
- data/benchmarks/allocation_profiling.rb +253 -0
- data/benchmarks/e11y_benchmarks.rb +447 -0
- data/benchmarks/ruby_baseline_allocations.rb +175 -0
- data/benchmarks/run_all.rb +9 -21
- data/docs/00-ICP-AND-TIMELINE.md +2 -2
- data/docs/ADR-001-architecture.md +1 -1
- data/docs/ADR-004-adapter-architecture.md +247 -0
- data/docs/ADR-009-cost-optimization.md +231 -115
- data/docs/ADR-017-multi-rails-compatibility.md +103 -0
- data/docs/ADR-INDEX.md +99 -0
- data/docs/CONTRIBUTING.md +312 -0
- data/docs/IMPLEMENTATION_PLAN.md +1 -1
- data/docs/QUICK-START.md +0 -6
- data/docs/use_cases/UC-019-retention-based-routing.md +584 -0
- data/e11y.gemspec +28 -17
- data/lib/e11y/adapters/adaptive_batcher.rb +3 -0
- data/lib/e11y/adapters/audit_encrypted.rb +10 -4
- data/lib/e11y/adapters/base.rb +15 -0
- data/lib/e11y/adapters/file.rb +4 -1
- data/lib/e11y/adapters/in_memory.rb +6 -0
- data/lib/e11y/adapters/loki.rb +9 -0
- data/lib/e11y/adapters/otel_logs.rb +11 -9
- data/lib/e11y/adapters/sentry.rb +9 -0
- data/lib/e11y/adapters/yabeda.rb +54 -10
- data/lib/e11y/buffers.rb +8 -8
- data/lib/e11y/console.rb +52 -60
- data/lib/e11y/event/base.rb +75 -10
- data/lib/e11y/event/value_sampling_config.rb +10 -4
- data/lib/e11y/events/rails/http/request.rb +1 -1
- data/lib/e11y/instruments/active_job.rb +6 -3
- data/lib/e11y/instruments/rails_instrumentation.rb +51 -28
- data/lib/e11y/instruments/sidekiq.rb +7 -7
- data/lib/e11y/logger/bridge.rb +24 -54
- data/lib/e11y/metrics/cardinality_protection.rb +257 -12
- data/lib/e11y/metrics/cardinality_tracker.rb +17 -0
- data/lib/e11y/metrics/registry.rb +6 -2
- data/lib/e11y/metrics/relabeling.rb +0 -56
- data/lib/e11y/metrics.rb +6 -1
- data/lib/e11y/middleware/audit_signing.rb +12 -9
- data/lib/e11y/middleware/pii_filter.rb +18 -10
- data/lib/e11y/middleware/request.rb +10 -4
- data/lib/e11y/middleware/routing.rb +117 -90
- data/lib/e11y/middleware/sampling.rb +47 -28
- data/lib/e11y/middleware/trace_context.rb +40 -11
- data/lib/e11y/middleware/validation.rb +20 -2
- data/lib/e11y/middleware/versioning.rb +1 -1
- data/lib/e11y/pii.rb +7 -7
- data/lib/e11y/railtie.rb +24 -20
- data/lib/e11y/reliability/circuit_breaker.rb +3 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +16 -5
- data/lib/e11y/reliability/dlq/filter.rb +3 -0
- data/lib/e11y/reliability/retry_handler.rb +4 -0
- data/lib/e11y/sampling/error_spike_detector.rb +16 -5
- data/lib/e11y/sampling/load_monitor.rb +13 -4
- data/lib/e11y/self_monitoring/reliability_monitor.rb +3 -0
- data/lib/e11y/version.rb +1 -1
- data/lib/e11y.rb +86 -9
- metadata +83 -38
- data/docs/use_cases/UC-019-tiered-storage-migration.md +0 -562
- data/lib/e11y/middleware/pii_filtering.rb +0 -280
- 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
|
|
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
|
-
# **
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# -
|
|
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
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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-
|
|
24
|
-
# @see ADR-
|
|
25
|
-
# @see UC-
|
|
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
|
|
26
|
+
# @example Explicit adapters (bypass routing)
|
|
28
27
|
# event_data = {
|
|
29
|
-
# event_name: '
|
|
30
|
-
#
|
|
31
|
-
#
|
|
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
|
-
#
|
|
36
|
-
#
|
|
37
|
-
# @example Debug event routing (request-scoped)
|
|
34
|
+
# @example Audit event routing (via rules)
|
|
38
35
|
# event_data = {
|
|
39
|
-
# event_name: '
|
|
40
|
-
#
|
|
41
|
-
#
|
|
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
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# @example Audit event routing
|
|
43
|
+
# @example Retention-based routing
|
|
48
44
|
# event_data = {
|
|
49
|
-
# event_name: '
|
|
50
|
-
#
|
|
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:
|
|
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
|
|
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
|
|
64
|
-
# @option event_data [
|
|
65
|
-
# @option event_data [Boolean] :audit_event Audit event flag (optional)
|
|
66
|
-
# @
|
|
67
|
-
#
|
|
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
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
|
96
|
-
log_routing_decision(event_data,
|
|
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 (
|
|
99
|
-
@app
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
108
|
-
#
|
|
109
|
-
#
|
|
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
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
129
|
-
# TODO: Replace with structured logging
|
|
130
|
-
# Rails.logger.debug "[E11y] Routing: #{event_data[:event_name]} → #{
|
|
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
|
|
168
|
+
# TODO: Read from configuration
|
|
138
169
|
# E11y.configuration.debug_enabled
|
|
139
|
-
false # Disabled
|
|
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
|
|
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(
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
@
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
58
|
-
event_data
|
|
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
|
-
|
|
61
|
-
event_data[:span_id] ||= generate_span_id
|
|
63
|
+
private
|
|
62
64
|
|
|
63
|
-
|
|
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
|
-
#
|
|
67
|
-
event_data[:timestamp]
|
|
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
|
-
#
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
38
|
-
config.service_name
|
|
39
|
-
|
|
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
|
-
#
|
|
44
|
-
|
|
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
|