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
@@ -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
- # 1. Validate payload against schema (respects validation_mode)
92
- validate_payload!(payload) if should_validate?
92
+ return unless E11y.config.enabled
93
93
 
94
- # 2. Build event hash with metadata (use pre-allocated template, reduce GC)
95
- # Cache frequently accessed values to avoid method call overhead
96
- event_severity = severity
97
- event_adapters = adapters
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
- # 3. TODO Phase 2: Send to pipeline
100
- # E11y::Pipeline.process(event_hash)
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
- # 4. Return event hash (pre-allocated structure for performance)
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: Time.now.utc.iso8601(3) # ISO8601 with milliseconds
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
- unless %i[greater_than less_than equals in_range].include?(type)
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 %i[greater_than less_than].include?(type) && !threshold.is_a?(Numeric)
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).maybe(:string)
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.start!
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.flush!
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, id, payload|
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
- # Track E11y event with extracted payload
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
- **extract_relevant_payload(payload)
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 relevant payload fields from ASN event
130
+ # Extract job info from job object (ActiveJob events)
113
131
  #
114
- # Filters out PII and noisy fields, keeping only relevant data.
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] Filtered payload
118
- def self.extract_relevant_payload(payload)
119
- # Extract only relevant fields (avoid PII, reduce noise)
120
- # This is a basic implementation - specific event classes can override
121
- payload.slice(
122
- :controller, :action, :format, :status,
123
- :allocations, :db_runtime, :view_runtime,
124
- :name, :sql, :connection_id,
125
- :key, :hit,
126
- :job_class, :job_id, :queue
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, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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.start!
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(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
- # Flush buffer on success (not on error, already flushed in rescue)
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.flush!
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}"
@@ -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. Optionally tracks log calls as E11y events (when enabled)
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 optionally track to E11y
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, &) if should_track_severity?(:debug)
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, &) if should_track_severity?(:info)
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, &) if should_track_severity?(:warn)
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, &) if should_track_severity?(:error)
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, &) if should_track_severity?(:fatal)
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, &) if should_track_severity?(e11y_severity)
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
- def track_to_e11y(severity, message = nil, &block)
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? ? block.call : nil)
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 do |loc|
198
- !loc.path.include?("e11y")
199
- end&.then do |loc|
200
- "#{loc.path}:#{loc.lineno}:in `#{loc.label}'"
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