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
data/lib/e11y/event/base.rb
CHANGED
|
@@ -78,6 +78,7 @@ module E11y
|
|
|
78
78
|
# - Cached severity/adapters (avoid repeated method calls)
|
|
79
79
|
# - Inline timestamp generation
|
|
80
80
|
# - Configurable validation mode (:always, :sampled, :never)
|
|
81
|
+
# - Auto-calculated retention_until from retention_period
|
|
81
82
|
#
|
|
82
83
|
# @param payload [Hash] Event data matching the schema
|
|
83
84
|
# @return [Hash] Event hash (includes metadata)
|
|
@@ -88,28 +89,57 @@ module E11y
|
|
|
88
89
|
#
|
|
89
90
|
# @raise [E11y::ValidationError] if payload doesn't match schema (when validation runs)
|
|
90
91
|
def track(**payload)
|
|
91
|
-
|
|
92
|
-
validate_payload!(payload) if should_validate?
|
|
92
|
+
return unless E11y.config.enabled
|
|
93
93
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
# Build event data hash for pipeline processing
|
|
95
|
+
event_data = {
|
|
96
|
+
event_class: self,
|
|
97
|
+
event_name: event_name,
|
|
98
|
+
payload: payload,
|
|
99
|
+
severity: severity,
|
|
100
|
+
version: version,
|
|
101
|
+
adapters: adapters,
|
|
102
|
+
timestamp: Time.now.utc,
|
|
103
|
+
retention_period: retention_period,
|
|
104
|
+
context: build_context
|
|
105
|
+
}
|
|
98
106
|
|
|
99
|
-
#
|
|
100
|
-
#
|
|
107
|
+
# Pass through middleware pipeline (ADR-001 §3.2)
|
|
108
|
+
# Pipeline handles: validation, PII filtering, rate limiting, sampling, routing
|
|
109
|
+
# Routing middleware is the LAST middleware and it writes to adapters directly
|
|
110
|
+
E11y.config.built_pipeline.call(event_data)
|
|
101
111
|
|
|
102
|
-
#
|
|
112
|
+
# Return event data for testing/debugging
|
|
113
|
+
event_data
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Build event hash
|
|
117
|
+
# @api private
|
|
118
|
+
def build_event_hash(event_severity, event_adapters, event_timestamp, event_retention_period, payload)
|
|
103
119
|
{
|
|
104
120
|
event_name: event_name,
|
|
105
121
|
payload: payload,
|
|
106
122
|
severity: event_severity,
|
|
107
123
|
version: version,
|
|
108
124
|
adapters: event_adapters,
|
|
109
|
-
timestamp:
|
|
125
|
+
timestamp: event_timestamp.iso8601(3), # ISO8601 with milliseconds
|
|
126
|
+
retention_until: (event_timestamp + event_retention_period).iso8601, # Auto-calculated
|
|
127
|
+
audit_event: audit_event? # For routing rules
|
|
110
128
|
}
|
|
111
129
|
end
|
|
112
130
|
|
|
131
|
+
# Build current context for event
|
|
132
|
+
# @api private
|
|
133
|
+
def build_context
|
|
134
|
+
{
|
|
135
|
+
trace_id: E11y::Current.trace_id,
|
|
136
|
+
span_id: E11y::Current.span_id,
|
|
137
|
+
parent_trace_id: E11y::Current.parent_trace_id,
|
|
138
|
+
request_id: E11y::Current.request_id,
|
|
139
|
+
user_id: E11y::Current.user_id
|
|
140
|
+
}.compact
|
|
141
|
+
end
|
|
142
|
+
|
|
113
143
|
# Configure validation mode for performance tuning
|
|
114
144
|
#
|
|
115
145
|
# Modes:
|
|
@@ -242,6 +272,41 @@ module E11y
|
|
|
242
272
|
1
|
|
243
273
|
end
|
|
244
274
|
|
|
275
|
+
# Set or get retention period for this event
|
|
276
|
+
#
|
|
277
|
+
# Retention period determines how long events should be kept in storage.
|
|
278
|
+
# Used by routing middleware to select appropriate adapters (hot/warm/cold storage).
|
|
279
|
+
#
|
|
280
|
+
# @param value [ActiveSupport::Duration, nil] Retention period (e.g., 30.days, 7.years)
|
|
281
|
+
# @return [ActiveSupport::Duration] Current retention period
|
|
282
|
+
#
|
|
283
|
+
# @example Short retention (debug logs)
|
|
284
|
+
# class DebugEvent < E11y::Event::Base
|
|
285
|
+
# retention_period 7.days
|
|
286
|
+
# end
|
|
287
|
+
#
|
|
288
|
+
# @example Long retention (audit events)
|
|
289
|
+
# class UserDeletedEvent < E11y::Event::Base
|
|
290
|
+
# audit_event true
|
|
291
|
+
# retention_period 7.years # GDPR compliance
|
|
292
|
+
# end
|
|
293
|
+
#
|
|
294
|
+
# @example Default retention (from config)
|
|
295
|
+
# class OrderEvent < E11y::Event::Base
|
|
296
|
+
# # No retention_period specified → uses config default (30 days)
|
|
297
|
+
# end
|
|
298
|
+
def retention_period(value = nil)
|
|
299
|
+
@retention_period = value if value
|
|
300
|
+
# Return explicitly set retention_period OR inherit from parent (if set) OR config default OR final fallback
|
|
301
|
+
return @retention_period if @retention_period
|
|
302
|
+
if superclass != E11y::Event::Base && superclass.instance_variable_get(:@retention_period)
|
|
303
|
+
return superclass.retention_period
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Fallback to configuration or 30 days
|
|
307
|
+
E11y.configuration&.default_retention_period || 30.days
|
|
308
|
+
end
|
|
309
|
+
|
|
245
310
|
# Set or get adapters for this event
|
|
246
311
|
#
|
|
247
312
|
# Adapters are referenced by NAME (e.g., :logs, :errors_tracker).
|
|
@@ -62,23 +62,29 @@ module E11y
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# Valid comparison types for value-based sampling
|
|
66
|
+
VALID_COMPARISON_TYPES = %i[greater_than less_than equals in_range].freeze
|
|
67
|
+
# Comparison types that require numeric thresholds
|
|
68
|
+
NUMERIC_COMPARISON_TYPES = %i[greater_than less_than].freeze
|
|
69
|
+
|
|
65
70
|
private
|
|
66
71
|
|
|
72
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
73
|
+
# Validation requires checking multiple comparison types and threshold types
|
|
67
74
|
def validate_comparisons!
|
|
68
75
|
raise ArgumentError, "At least one comparison required" if comparisons.empty?
|
|
69
76
|
|
|
70
77
|
comparisons.each do |type, threshold|
|
|
71
|
-
|
|
72
|
-
raise ArgumentError, "Invalid comparison type: #{type}"
|
|
73
|
-
end
|
|
78
|
+
raise ArgumentError, "Invalid comparison type: #{type}" unless VALID_COMPARISON_TYPES.include?(type)
|
|
74
79
|
|
|
75
80
|
raise ArgumentError, "in_range requires a Range" if type == :in_range && !threshold.is_a?(Range)
|
|
76
81
|
|
|
77
|
-
if
|
|
82
|
+
if NUMERIC_COMPARISON_TYPES.include?(type) && !threshold.is_a?(Numeric)
|
|
78
83
|
raise ArgumentError, "#{type} requires a Numeric threshold"
|
|
79
84
|
end
|
|
80
85
|
end
|
|
81
86
|
end
|
|
87
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
end
|
|
@@ -11,7 +11,7 @@ module E11y
|
|
|
11
11
|
required(:duration).filled(:float)
|
|
12
12
|
optional(:controller).maybe(:string)
|
|
13
13
|
optional(:action).maybe(:string)
|
|
14
|
-
optional(:format).
|
|
14
|
+
optional(:format) # Rails passes Symbol (e.g., :html, :json)
|
|
15
15
|
optional(:status).maybe(:integer)
|
|
16
16
|
optional(:view_runtime).maybe(:float)
|
|
17
17
|
optional(:db_runtime).maybe(:float)
|
|
@@ -85,7 +85,7 @@ module E11y
|
|
|
85
85
|
def setup_job_buffer_active_job
|
|
86
86
|
return unless E11y.config.request_buffer&.enabled
|
|
87
87
|
|
|
88
|
-
E11y::Buffers::RequestScopedBuffer.
|
|
88
|
+
E11y::Buffers::RequestScopedBuffer.initialize!
|
|
89
89
|
rescue StandardError => e
|
|
90
90
|
# C18: Don't fail job if buffer setup fails
|
|
91
91
|
warn "[E11y] Failed to start job buffer: #{e.message}"
|
|
@@ -95,7 +95,7 @@ module E11y
|
|
|
95
95
|
def handle_job_error_active_job(_error)
|
|
96
96
|
return unless E11y.config.request_buffer&.enabled
|
|
97
97
|
|
|
98
|
-
E11y::Buffers::RequestScopedBuffer.flush_on_error
|
|
98
|
+
E11y::Buffers::RequestScopedBuffer.flush_on_error
|
|
99
99
|
rescue StandardError => e
|
|
100
100
|
# C18: Don't fail job if buffer flush fails
|
|
101
101
|
warn "[E11y] Failed to flush job buffer on error: #{e.message}"
|
|
@@ -106,7 +106,7 @@ module E11y
|
|
|
106
106
|
# Flush buffer on success (not on error, already flushed in rescue)
|
|
107
107
|
if !$ERROR_INFO && E11y.config.request_buffer&.enabled
|
|
108
108
|
begin
|
|
109
|
-
E11y::Buffers::RequestScopedBuffer.
|
|
109
|
+
E11y::Buffers::RequestScopedBuffer.discard
|
|
110
110
|
rescue StandardError => e
|
|
111
111
|
# C18: Don't fail job if buffer flush fails
|
|
112
112
|
warn "[E11y] Failed to flush job buffer: #{e.message}"
|
|
@@ -139,6 +139,8 @@ module E11y
|
|
|
139
139
|
# @param start_time [Time] Job start time
|
|
140
140
|
# @return [void]
|
|
141
141
|
# @api private
|
|
142
|
+
# rubocop:disable Metrics/AbcSize
|
|
143
|
+
# SLO tracking requires config check, duration calculation, method call, and error handling
|
|
142
144
|
def track_job_slo_active_job(job, status, start_time)
|
|
143
145
|
return unless E11y.config.slo_tracking&.enabled
|
|
144
146
|
|
|
@@ -155,6 +157,7 @@ module E11y
|
|
|
155
157
|
# C18: Don't fail if SLO tracking fails
|
|
156
158
|
E11y.logger.warn("[E11y] SLO tracking error: #{e.message}", error: e.class.name)
|
|
157
159
|
end
|
|
160
|
+
# rubocop:enable Metrics/AbcSize
|
|
158
161
|
end
|
|
159
162
|
|
|
160
163
|
# Custom attribute accessors for trace context (C17 Hybrid Tracing)
|
|
@@ -30,18 +30,18 @@ module E11y
|
|
|
30
30
|
#
|
|
31
31
|
# @return [Hash<String, Class>] Event mappings
|
|
32
32
|
DEFAULT_RAILS_EVENT_MAPPING = {
|
|
33
|
-
"sql.active_record" => "Events::Rails::Database::Query",
|
|
34
|
-
"process_action.action_controller" => "Events::Rails::Http::Request",
|
|
35
|
-
"render_template.action_view" => "Events::Rails::View::Render",
|
|
36
|
-
"send_file.action_controller" => "Events::Rails::Http::SendFile",
|
|
37
|
-
"redirect_to.action_controller" => "Events::Rails::Http::Redirect",
|
|
38
|
-
"cache_read.active_support" => "Events::Rails::Cache::Read",
|
|
39
|
-
"cache_write.active_support" => "Events::Rails::Cache::Write",
|
|
40
|
-
"cache_delete.active_support" => "Events::Rails::Cache::Delete",
|
|
41
|
-
"enqueue.active_job" => "Events::Rails::Job::Enqueued",
|
|
42
|
-
"enqueue_at.active_job" => "Events::Rails::Job::Scheduled",
|
|
43
|
-
"perform_start.active_job" => "Events::Rails::Job::Started",
|
|
44
|
-
"perform.active_job" => "Events::Rails::Job::Completed"
|
|
33
|
+
"sql.active_record" => "E11y::Events::Rails::Database::Query",
|
|
34
|
+
"process_action.action_controller" => "E11y::Events::Rails::Http::Request",
|
|
35
|
+
"render_template.action_view" => "E11y::Events::Rails::View::Render",
|
|
36
|
+
"send_file.action_controller" => "E11y::Events::Rails::Http::SendFile",
|
|
37
|
+
"redirect_to.action_controller" => "E11y::Events::Rails::Http::Redirect",
|
|
38
|
+
"cache_read.active_support" => "E11y::Events::Rails::Cache::Read",
|
|
39
|
+
"cache_write.active_support" => "E11y::Events::Rails::Cache::Write",
|
|
40
|
+
"cache_delete.active_support" => "E11y::Events::Rails::Cache::Delete",
|
|
41
|
+
"enqueue.active_job" => "E11y::Events::Rails::Job::Enqueued",
|
|
42
|
+
"enqueue_at.active_job" => "E11y::Events::Rails::Job::Scheduled",
|
|
43
|
+
"perform_start.active_job" => "E11y::Events::Rails::Job::Started",
|
|
44
|
+
"perform.active_job" => "E11y::Events::Rails::Job::Completed"
|
|
45
45
|
}.freeze
|
|
46
46
|
|
|
47
47
|
# Setup Rails instrumentation
|
|
@@ -61,11 +61,26 @@ module E11y
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# Subscribe to a specific ASN event
|
|
64
|
+
#
|
|
65
|
+
# **Architecture Note**: We pass the entire payload to the event class.
|
|
66
|
+
# Each event class defines its own schema (via dry-schema), which:
|
|
67
|
+
# 1. **Type-checks** fields automatically
|
|
68
|
+
# 2. **Filters** only relevant fields (unknown fields ignored)
|
|
69
|
+
# 3. **Validates** payload structure
|
|
70
|
+
#
|
|
71
|
+
# This eliminates the need for manual whitelisting (DRY principle).
|
|
72
|
+
# PII filtering happens in the middleware pipeline, not here.
|
|
73
|
+
#
|
|
64
74
|
# @param asn_pattern [String] ActiveSupport::Notifications pattern
|
|
65
75
|
# @param e11y_event_class_name [String] E11y event class name
|
|
66
76
|
# @return [void]
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# # ASN payload: { controller: "Users", action: "index", password: "secret" }
|
|
80
|
+
# # Event schema: schema { required(:controller).string; required(:action).string }
|
|
81
|
+
# # Result: { controller: "Users", action: "index" } - password filtered by schema
|
|
67
82
|
def self.subscribe_to_event(asn_pattern, e11y_event_class_name)
|
|
68
|
-
ActiveSupport::Notifications.subscribe(asn_pattern) do |name, start, finish,
|
|
83
|
+
ActiveSupport::Notifications.subscribe(asn_pattern) do |name, start, finish, _id, payload|
|
|
69
84
|
# Convert ASN event → E11y event
|
|
70
85
|
duration = (finish - start) * 1000 # Convert to milliseconds
|
|
71
86
|
|
|
@@ -73,11 +88,14 @@ module E11y
|
|
|
73
88
|
e11y_event_class = resolve_event_class(e11y_event_class_name)
|
|
74
89
|
next unless e11y_event_class
|
|
75
90
|
|
|
76
|
-
#
|
|
91
|
+
# Extract job info from job object if present (ActiveJob events)
|
|
92
|
+
extracted_payload = extract_job_info_from_object(payload)
|
|
93
|
+
|
|
94
|
+
# Track E11y event - schema will filter relevant fields
|
|
77
95
|
e11y_event_class.track(
|
|
78
96
|
event_name: name,
|
|
79
97
|
duration: duration,
|
|
80
|
-
**
|
|
98
|
+
**extracted_payload # Pass all payload, schema filters
|
|
81
99
|
)
|
|
82
100
|
rescue StandardError => e
|
|
83
101
|
# Don't crash the app if event tracking fails
|
|
@@ -109,22 +127,27 @@ module E11y
|
|
|
109
127
|
ignore_list.include?(pattern)
|
|
110
128
|
end
|
|
111
129
|
|
|
112
|
-
# Extract
|
|
130
|
+
# Extract job info from job object (ActiveJob events)
|
|
113
131
|
#
|
|
114
|
-
#
|
|
132
|
+
# ActiveJob events pass job object, not flattened fields.
|
|
133
|
+
# This method extracts relevant fields from job object.
|
|
115
134
|
#
|
|
116
135
|
# @param payload [Hash] ASN event payload
|
|
117
|
-
# @return [Hash]
|
|
118
|
-
def self.
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
# @return [Hash] Payload with extracted job info
|
|
137
|
+
def self.extract_job_info_from_object(payload)
|
|
138
|
+
# Return early if no job object
|
|
139
|
+
return payload unless payload[:job]
|
|
140
|
+
|
|
141
|
+
# Clone payload to avoid mutation
|
|
142
|
+
result = payload.dup
|
|
143
|
+
job = result.delete(:job)
|
|
144
|
+
|
|
145
|
+
# Extract job fields if not already present
|
|
146
|
+
result[:job_class] ||= job.class.name
|
|
147
|
+
result[:job_id] ||= job.job_id
|
|
148
|
+
result[:queue] ||= job.queue_name
|
|
149
|
+
|
|
150
|
+
result
|
|
128
151
|
end
|
|
129
152
|
|
|
130
153
|
# Resolve event class from string name
|
|
@@ -43,7 +43,7 @@ module E11y
|
|
|
43
43
|
# **C17 Hybrid Tracing**: Creates NEW trace_id for job, but preserves parent link.
|
|
44
44
|
# **C18 Non-Failing**: E11y errors don't fail jobs (observability is secondary to business logic).
|
|
45
45
|
class ServerMiddleware
|
|
46
|
-
# rubocop:disable Metrics/AbcSize
|
|
46
|
+
# rubocop:disable Metrics/AbcSize
|
|
47
47
|
def call(_worker, job, queue)
|
|
48
48
|
# C18: Disable fail_on_error for jobs (observability should not block business logic)
|
|
49
49
|
original_fail_on_error = E11y.config.error_handling.fail_on_error
|
|
@@ -73,7 +73,7 @@ module E11y
|
|
|
73
73
|
# Restore original setting
|
|
74
74
|
E11y.config.error_handling.fail_on_error = original_fail_on_error
|
|
75
75
|
end
|
|
76
|
-
# rubocop:enable Metrics/AbcSize
|
|
76
|
+
# rubocop:enable Metrics/AbcSize
|
|
77
77
|
|
|
78
78
|
private
|
|
79
79
|
|
|
@@ -97,18 +97,18 @@ module E11y
|
|
|
97
97
|
def setup_job_buffer
|
|
98
98
|
return unless E11y.config.request_buffer&.enabled
|
|
99
99
|
|
|
100
|
-
E11y::Buffers::RequestScopedBuffer.
|
|
100
|
+
E11y::Buffers::RequestScopedBuffer.initialize!
|
|
101
101
|
rescue StandardError => e
|
|
102
102
|
# C18: Don't fail job if buffer setup fails
|
|
103
103
|
warn "[E11y] Failed to start job buffer: #{e.message}"
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# Handle job error (C18: Non-Failing Event Tracking)
|
|
107
|
-
def handle_job_error(
|
|
107
|
+
def handle_job_error(_error)
|
|
108
108
|
# Flush buffer on error (includes debug events)
|
|
109
109
|
return unless E11y.config.request_buffer&.enabled
|
|
110
110
|
|
|
111
|
-
E11y::Buffers::RequestScopedBuffer.flush_on_error
|
|
111
|
+
E11y::Buffers::RequestScopedBuffer.flush_on_error
|
|
112
112
|
rescue StandardError => e
|
|
113
113
|
# C18: Don't fail job if buffer flush fails
|
|
114
114
|
warn "[E11y] Failed to flush job buffer on error: #{e.message}"
|
|
@@ -116,10 +116,10 @@ module E11y
|
|
|
116
116
|
|
|
117
117
|
# Cleanup job-scoped context
|
|
118
118
|
def cleanup_job_context
|
|
119
|
-
#
|
|
119
|
+
# Discard buffer on success (not on error, already flushed in rescue)
|
|
120
120
|
if !$ERROR_INFO && E11y.config.request_buffer&.enabled
|
|
121
121
|
begin
|
|
122
|
-
E11y::Buffers::RequestScopedBuffer.
|
|
122
|
+
E11y::Buffers::RequestScopedBuffer.discard
|
|
123
123
|
rescue StandardError => e
|
|
124
124
|
# C18: Don't fail job if buffer flush fails
|
|
125
125
|
warn "[E11y] Failed to flush job buffer: #{e.message}"
|
data/lib/e11y/logger/bridge.rb
CHANGED
|
@@ -8,7 +8,7 @@ module E11y
|
|
|
8
8
|
#
|
|
9
9
|
# Transparent wrapper around Rails.logger that:
|
|
10
10
|
# 1. Delegates all calls to the original logger (preserves Rails behavior)
|
|
11
|
-
# 2.
|
|
11
|
+
# 2. Tracks log calls as E11y events (when logger_bridge.enabled = true)
|
|
12
12
|
#
|
|
13
13
|
# **Why SimpleDelegator instead of full replacement:**
|
|
14
14
|
# - ✅ Simpler: No need to reimplement entire Logger API
|
|
@@ -22,8 +22,7 @@ module E11y
|
|
|
22
22
|
#
|
|
23
23
|
# @example Manual setup
|
|
24
24
|
# E11y.configure do |config|
|
|
25
|
-
# config.logger_bridge.enabled = true
|
|
26
|
-
# config.logger_bridge.track_to_e11y = true # Send logs to E11y events (optional)
|
|
25
|
+
# config.logger_bridge.enabled = true # Wrap Rails.logger and send logs to E11y
|
|
27
26
|
# end
|
|
28
27
|
#
|
|
29
28
|
# @see ADR-008 §7 (Rails.logger Migration)
|
|
@@ -36,10 +35,10 @@ module E11y
|
|
|
36
35
|
# @return [void]
|
|
37
36
|
def self.setup!
|
|
38
37
|
return unless E11y.config.logger_bridge&.enabled
|
|
39
|
-
return unless defined?(Rails)
|
|
38
|
+
return unless defined?(::Rails)
|
|
40
39
|
|
|
41
40
|
# Wrap Rails.logger (preserves original behavior)
|
|
42
|
-
Rails.logger = Bridge.new(Rails.logger)
|
|
41
|
+
::Rails.logger = Bridge.new(::Rails.logger)
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
# Initialize bridge wrapper
|
|
@@ -56,7 +55,7 @@ module E11y
|
|
|
56
55
|
}
|
|
57
56
|
end
|
|
58
57
|
|
|
59
|
-
# Intercept logger methods to
|
|
58
|
+
# Intercept logger methods to track to E11y
|
|
60
59
|
# All calls are delegated to the original logger via SimpleDelegator
|
|
61
60
|
|
|
62
61
|
# Log debug message
|
|
@@ -64,7 +63,7 @@ module E11y
|
|
|
64
63
|
# @yield Block that returns log message
|
|
65
64
|
# @return [true] Always returns true (Logger API)
|
|
66
65
|
def debug(message = nil, &)
|
|
67
|
-
track_to_e11y(:debug, message, &)
|
|
66
|
+
track_to_e11y(:debug, message, &)
|
|
68
67
|
super # Delegate to original logger
|
|
69
68
|
end
|
|
70
69
|
|
|
@@ -73,7 +72,7 @@ module E11y
|
|
|
73
72
|
# @yield Block that returns log message
|
|
74
73
|
# @return [true] Always returns true (Logger API)
|
|
75
74
|
def info(message = nil, &)
|
|
76
|
-
track_to_e11y(:info, message, &)
|
|
75
|
+
track_to_e11y(:info, message, &)
|
|
77
76
|
super # Delegate to original logger
|
|
78
77
|
end
|
|
79
78
|
|
|
@@ -82,7 +81,7 @@ module E11y
|
|
|
82
81
|
# @yield Block that returns log message
|
|
83
82
|
# @return [true] Always returns true (Logger API)
|
|
84
83
|
def warn(message = nil, &)
|
|
85
|
-
track_to_e11y(:warn, message, &)
|
|
84
|
+
track_to_e11y(:warn, message, &)
|
|
86
85
|
super # Delegate to original logger
|
|
87
86
|
end
|
|
88
87
|
|
|
@@ -91,7 +90,7 @@ module E11y
|
|
|
91
90
|
# @yield Block that returns log message
|
|
92
91
|
# @return [true] Always returns true (Logger API)
|
|
93
92
|
def error(message = nil, &)
|
|
94
|
-
track_to_e11y(:error, message, &)
|
|
93
|
+
track_to_e11y(:error, message, &)
|
|
95
94
|
super # Delegate to original logger
|
|
96
95
|
end
|
|
97
96
|
|
|
@@ -100,7 +99,7 @@ module E11y
|
|
|
100
99
|
# @yield Block that returns log message
|
|
101
100
|
# @return [true] Always returns true (Logger API)
|
|
102
101
|
def fatal(message = nil, &)
|
|
103
|
-
track_to_e11y(:fatal, message, &)
|
|
102
|
+
track_to_e11y(:fatal, message, &)
|
|
104
103
|
super # Delegate to original logger
|
|
105
104
|
end
|
|
106
105
|
|
|
@@ -112,7 +111,7 @@ module E11y
|
|
|
112
111
|
# @return [true] Always returns true (Logger API)
|
|
113
112
|
def add(severity, message = nil, progname = nil, &)
|
|
114
113
|
e11y_severity = @severity_mapping[severity] || :info
|
|
115
|
-
track_to_e11y(e11y_severity, message || progname, &)
|
|
114
|
+
track_to_e11y(e11y_severity, message || progname, &)
|
|
116
115
|
super # Delegate to original logger
|
|
117
116
|
end
|
|
118
117
|
|
|
@@ -120,48 +119,16 @@ module E11y
|
|
|
120
119
|
|
|
121
120
|
private
|
|
122
121
|
|
|
123
|
-
# Check if E11y tracking is enabled for specific severity
|
|
124
|
-
# Supports both boolean and per-severity Hash configuration
|
|
125
|
-
#
|
|
126
|
-
# @param severity [Symbol] E11y severity (:debug, :info, :warn, :error, :fatal)
|
|
127
|
-
# @return [Boolean]
|
|
128
|
-
#
|
|
129
|
-
# @example Boolean config (all or nothing)
|
|
130
|
-
# config.logger_bridge.track_to_e11y = true # Track all
|
|
131
|
-
# config.logger_bridge.track_to_e11y = false # Track none
|
|
132
|
-
#
|
|
133
|
-
# @example Per-severity config (granular control)
|
|
134
|
-
# config.logger_bridge.track_to_e11y = {
|
|
135
|
-
# debug: false,
|
|
136
|
-
# info: true,
|
|
137
|
-
# warn: true,
|
|
138
|
-
# error: true,
|
|
139
|
-
# fatal: true
|
|
140
|
-
# }
|
|
141
|
-
def should_track_severity?(severity)
|
|
142
|
-
config = E11y.config.logger_bridge&.track_to_e11y
|
|
143
|
-
return false unless config
|
|
144
|
-
|
|
145
|
-
case config
|
|
146
|
-
when TrueClass
|
|
147
|
-
true # Track all severities
|
|
148
|
-
when FalseClass
|
|
149
|
-
false # Track none
|
|
150
|
-
when Hash
|
|
151
|
-
config[severity] || false # Check per-severity config
|
|
152
|
-
else
|
|
153
|
-
false # Unknown config type
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
122
|
# Track log message as E11y event
|
|
158
123
|
# @param severity [Symbol] E11y severity
|
|
159
124
|
# @param message [String, nil] Log message
|
|
160
125
|
# @yield Block that returns log message
|
|
161
126
|
# @return [void]
|
|
162
|
-
|
|
127
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
128
|
+
# Logger tracking requires message extraction, validation, event class lookup, and error handling
|
|
129
|
+
def track_to_e11y(severity, message = nil)
|
|
163
130
|
# Extract message
|
|
164
|
-
msg = message || (block_given? ?
|
|
131
|
+
msg = message || (block_given? ? yield : nil)
|
|
165
132
|
return if msg.nil? || (msg.respond_to?(:empty?) && msg.empty?)
|
|
166
133
|
|
|
167
134
|
# Track to E11y using severity-specific class
|
|
@@ -174,12 +141,15 @@ module E11y
|
|
|
174
141
|
rescue StandardError => e
|
|
175
142
|
# Silently ignore E11y tracking errors (don't break logging!)
|
|
176
143
|
# In development/test, you might want to log this
|
|
177
|
-
warn "E11y logger tracking failed: #{e.message}" if defined?(Rails) && Rails.env.development?
|
|
144
|
+
warn "E11y logger tracking failed: #{e.message}" if defined?(::Rails) && ::Rails.env.development?
|
|
178
145
|
end
|
|
146
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
179
147
|
|
|
180
148
|
# Get event class for severity
|
|
181
149
|
# @param severity [Symbol] E11y severity
|
|
182
150
|
# @return [Class] Event class
|
|
151
|
+
# rubocop:disable Lint/DuplicateBranch
|
|
152
|
+
# Unknown severities intentionally fallback to Info (same as :info)
|
|
183
153
|
def event_class_for_severity(severity)
|
|
184
154
|
case severity
|
|
185
155
|
when :debug then E11y::Events::Rails::Log::Debug
|
|
@@ -190,15 +160,15 @@ module E11y
|
|
|
190
160
|
else E11y::Events::Rails::Log::Info # Fallback
|
|
191
161
|
end
|
|
192
162
|
end
|
|
163
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
193
164
|
|
|
194
165
|
# Extract caller location (first caller outside E11y)
|
|
195
166
|
# @return [String, nil] Caller location string
|
|
196
167
|
def extract_caller_location
|
|
197
|
-
caller_locations.find
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
end
|
|
168
|
+
loc = caller_locations.find { |l| !l.path.include?("e11y") }
|
|
169
|
+
return nil unless loc
|
|
170
|
+
|
|
171
|
+
"#{loc.path}:#{loc.lineno}:in `#{loc.label}'"
|
|
202
172
|
end
|
|
203
173
|
end
|
|
204
174
|
end
|