e11y 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rubocop.yml +69 -0
  4. data/CHANGELOG.md +26 -0
  5. data/CODE_OF_CONDUCT.md +64 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +179 -0
  8. data/Rakefile +37 -0
  9. data/benchmarks/run_all.rb +33 -0
  10. data/config/README.md +83 -0
  11. data/config/loki-local-config.yaml +35 -0
  12. data/config/prometheus.yml +15 -0
  13. data/docker-compose.yml +78 -0
  14. data/docs/00-ICP-AND-TIMELINE.md +483 -0
  15. data/docs/01-SCALE-REQUIREMENTS.md +858 -0
  16. data/docs/ADR-001-architecture.md +2617 -0
  17. data/docs/ADR-002-metrics-yabeda.md +1395 -0
  18. data/docs/ADR-003-slo-observability.md +3337 -0
  19. data/docs/ADR-004-adapter-architecture.md +2385 -0
  20. data/docs/ADR-005-tracing-context.md +1372 -0
  21. data/docs/ADR-006-security-compliance.md +4143 -0
  22. data/docs/ADR-007-opentelemetry-integration.md +1385 -0
  23. data/docs/ADR-008-rails-integration.md +1911 -0
  24. data/docs/ADR-009-cost-optimization.md +2993 -0
  25. data/docs/ADR-010-developer-experience.md +2166 -0
  26. data/docs/ADR-011-testing-strategy.md +1836 -0
  27. data/docs/ADR-012-event-evolution.md +958 -0
  28. data/docs/ADR-013-reliability-error-handling.md +2750 -0
  29. data/docs/ADR-014-event-driven-slo.md +1533 -0
  30. data/docs/ADR-015-middleware-order.md +1061 -0
  31. data/docs/ADR-016-self-monitoring-slo.md +1234 -0
  32. data/docs/API-REFERENCE-L28.md +914 -0
  33. data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
  34. data/docs/IMPLEMENTATION_NOTES.md +2804 -0
  35. data/docs/IMPLEMENTATION_PLAN.md +1971 -0
  36. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
  37. data/docs/PLAN.md +148 -0
  38. data/docs/QUICK-START.md +934 -0
  39. data/docs/README.md +296 -0
  40. data/docs/design/00-memory-optimization.md +593 -0
  41. data/docs/guides/MIGRATION-L27-L28.md +692 -0
  42. data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
  43. data/docs/guides/README.md +44 -0
  44. data/docs/prd/01-overview-vision.md +440 -0
  45. data/docs/use_cases/README.md +119 -0
  46. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
  47. data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
  48. data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
  49. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
  50. data/docs/use_cases/UC-005-sentry-integration.md +759 -0
  51. data/docs/use_cases/UC-006-trace-context-management.md +905 -0
  52. data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
  53. data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
  54. data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
  55. data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
  56. data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
  57. data/docs/use_cases/UC-012-audit-trail.md +2301 -0
  58. data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
  59. data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
  60. data/docs/use_cases/UC-015-cost-optimization.md +735 -0
  61. data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
  62. data/docs/use_cases/UC-017-local-development.md +867 -0
  63. data/docs/use_cases/UC-018-testing-events.md +1081 -0
  64. data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
  65. data/docs/use_cases/UC-020-event-versioning.md +708 -0
  66. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
  67. data/docs/use_cases/UC-022-event-registry.md +648 -0
  68. data/docs/use_cases/backlog.md +226 -0
  69. data/e11y.gemspec +76 -0
  70. data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
  71. data/lib/e11y/adapters/audit_encrypted.rb +239 -0
  72. data/lib/e11y/adapters/base.rb +580 -0
  73. data/lib/e11y/adapters/file.rb +224 -0
  74. data/lib/e11y/adapters/in_memory.rb +216 -0
  75. data/lib/e11y/adapters/loki.rb +333 -0
  76. data/lib/e11y/adapters/otel_logs.rb +203 -0
  77. data/lib/e11y/adapters/registry.rb +141 -0
  78. data/lib/e11y/adapters/sentry.rb +230 -0
  79. data/lib/e11y/adapters/stdout.rb +108 -0
  80. data/lib/e11y/adapters/yabeda.rb +370 -0
  81. data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
  82. data/lib/e11y/buffers/base_buffer.rb +40 -0
  83. data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
  84. data/lib/e11y/buffers/ring_buffer.rb +267 -0
  85. data/lib/e11y/buffers.rb +14 -0
  86. data/lib/e11y/console.rb +122 -0
  87. data/lib/e11y/current.rb +48 -0
  88. data/lib/e11y/event/base.rb +894 -0
  89. data/lib/e11y/event/value_sampling_config.rb +84 -0
  90. data/lib/e11y/events/base_audit_event.rb +43 -0
  91. data/lib/e11y/events/base_payment_event.rb +33 -0
  92. data/lib/e11y/events/rails/cache/delete.rb +21 -0
  93. data/lib/e11y/events/rails/cache/read.rb +23 -0
  94. data/lib/e11y/events/rails/cache/write.rb +22 -0
  95. data/lib/e11y/events/rails/database/query.rb +45 -0
  96. data/lib/e11y/events/rails/http/redirect.rb +21 -0
  97. data/lib/e11y/events/rails/http/request.rb +26 -0
  98. data/lib/e11y/events/rails/http/send_file.rb +21 -0
  99. data/lib/e11y/events/rails/http/start_processing.rb +26 -0
  100. data/lib/e11y/events/rails/job/completed.rb +22 -0
  101. data/lib/e11y/events/rails/job/enqueued.rb +22 -0
  102. data/lib/e11y/events/rails/job/failed.rb +22 -0
  103. data/lib/e11y/events/rails/job/scheduled.rb +23 -0
  104. data/lib/e11y/events/rails/job/started.rb +22 -0
  105. data/lib/e11y/events/rails/log.rb +56 -0
  106. data/lib/e11y/events/rails/view/render.rb +23 -0
  107. data/lib/e11y/events.rb +18 -0
  108. data/lib/e11y/instruments/active_job.rb +201 -0
  109. data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
  110. data/lib/e11y/instruments/sidekiq.rb +175 -0
  111. data/lib/e11y/logger/bridge.rb +205 -0
  112. data/lib/e11y/metrics/cardinality_protection.rb +172 -0
  113. data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
  114. data/lib/e11y/metrics/registry.rb +234 -0
  115. data/lib/e11y/metrics/relabeling.rb +226 -0
  116. data/lib/e11y/metrics.rb +102 -0
  117. data/lib/e11y/middleware/audit_signing.rb +174 -0
  118. data/lib/e11y/middleware/base.rb +140 -0
  119. data/lib/e11y/middleware/event_slo.rb +167 -0
  120. data/lib/e11y/middleware/pii_filter.rb +266 -0
  121. data/lib/e11y/middleware/pii_filtering.rb +280 -0
  122. data/lib/e11y/middleware/rate_limiting.rb +214 -0
  123. data/lib/e11y/middleware/request.rb +163 -0
  124. data/lib/e11y/middleware/routing.rb +157 -0
  125. data/lib/e11y/middleware/sampling.rb +254 -0
  126. data/lib/e11y/middleware/slo.rb +168 -0
  127. data/lib/e11y/middleware/trace_context.rb +131 -0
  128. data/lib/e11y/middleware/validation.rb +118 -0
  129. data/lib/e11y/middleware/versioning.rb +132 -0
  130. data/lib/e11y/middleware.rb +12 -0
  131. data/lib/e11y/pii/patterns.rb +90 -0
  132. data/lib/e11y/pii.rb +13 -0
  133. data/lib/e11y/pipeline/builder.rb +155 -0
  134. data/lib/e11y/pipeline/zone_validator.rb +110 -0
  135. data/lib/e11y/pipeline.rb +12 -0
  136. data/lib/e11y/presets/audit_event.rb +65 -0
  137. data/lib/e11y/presets/debug_event.rb +34 -0
  138. data/lib/e11y/presets/high_value_event.rb +51 -0
  139. data/lib/e11y/presets.rb +19 -0
  140. data/lib/e11y/railtie.rb +138 -0
  141. data/lib/e11y/reliability/circuit_breaker.rb +216 -0
  142. data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
  143. data/lib/e11y/reliability/dlq/filter.rb +117 -0
  144. data/lib/e11y/reliability/retry_handler.rb +207 -0
  145. data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
  146. data/lib/e11y/sampling/error_spike_detector.rb +225 -0
  147. data/lib/e11y/sampling/load_monitor.rb +161 -0
  148. data/lib/e11y/sampling/stratified_tracker.rb +92 -0
  149. data/lib/e11y/sampling/value_extractor.rb +82 -0
  150. data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
  151. data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
  152. data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
  153. data/lib/e11y/slo/event_driven.rb +150 -0
  154. data/lib/e11y/slo/tracker.rb +119 -0
  155. data/lib/e11y/version.rb +9 -0
  156. data/lib/e11y.rb +283 -0
  157. metadata +452 -0
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "json"
5
+
6
+ module E11y
7
+ module Middleware
8
+ # Audit Signing Middleware - HMAC-SHA256 signatures for audit events
9
+ #
10
+ # Signs audit events with HMAC-SHA256 before any transformations (including PII filtering).
11
+ # This ensures cryptographic proof of event authenticity for compliance.
12
+ #
13
+ # **Critical Design:**
14
+ # - Signs ORIGINAL data (before PII filtering) for legal compliance
15
+ # - Only processes events marked as audit events
16
+ # - Runs in :security zone BEFORE other middleware
17
+ #
18
+ # @example Audit Event
19
+ # class Events::UserDeleted < E11y::Event::Base
20
+ # audit_event true
21
+ #
22
+ # schema do
23
+ # required(:user_id).filled(:integer)
24
+ # required(:deleted_by).filled(:integer)
25
+ # required(:ip_address).filled(:string)
26
+ # end
27
+ # end
28
+ #
29
+ # Events::UserDeleted.track(
30
+ # user_id: 123,
31
+ # deleted_by: 456,
32
+ # ip_address: "192.168.1.1" # Original IP preserved
33
+ # )
34
+ #
35
+ # # Result: Signed with HMAC-SHA256 before PII filtering
36
+ #
37
+ # @see ADR-006 §4.0 Audit Trail Security
38
+ # @see UC-012 Audit Trail
39
+ class AuditSigning < Base
40
+ middleware_zone :security
41
+
42
+ # HMAC signing key (from ENV or generated)
43
+ SIGNING_KEY = ENV.fetch("E11Y_AUDIT_SIGNING_KEY") do
44
+ # Development fallback (NOT for production!)
45
+ if defined?(Rails) && Rails.env.production?
46
+ raise E11y::Error, "E11Y_AUDIT_SIGNING_KEY must be set in production"
47
+ end
48
+
49
+ "development_key_#{SecureRandom.hex(32)}"
50
+ end
51
+
52
+ # Initialize audit signing middleware
53
+ #
54
+ # @param app [Proc] Next middleware in chain
55
+
56
+ # Process event and sign if it's an audit event
57
+ #
58
+ # @param event_data [Hash] Event data with payload
59
+ # @return [Hash] Event data with signature
60
+ def call(event_data)
61
+ # Only sign audit events that require signing
62
+ if audit_event?(event_data) && requires_signing?(event_data)
63
+ signed_data = sign_event(event_data)
64
+ @app.call(signed_data)
65
+ else
66
+ # Non-audit events OR signing disabled: pass through
67
+ @app.call(event_data)
68
+ end
69
+ end
70
+
71
+ # Verify signature (for testing/validation)
72
+ #
73
+ # @param event_data [Hash] Event data with signature
74
+ # @return [Boolean] true if signature is valid
75
+ # rubocop:disable Naming/PredicateMethod
76
+ def self.verify_signature(event_data)
77
+ expected_signature = event_data[:audit_signature]
78
+ canonical = event_data[:audit_canonical]
79
+
80
+ return false unless expected_signature && canonical
81
+
82
+ actual_signature = OpenSSL::HMAC.hexdigest("SHA256", SIGNING_KEY, canonical)
83
+ actual_signature == expected_signature
84
+ end
85
+ # rubocop:enable Naming/PredicateMethod
86
+
87
+ private
88
+
89
+ # Check if event is marked as audit event
90
+ #
91
+ # @param event_data [Hash] Event data
92
+ # @return [Boolean] true if audit event
93
+ def audit_event?(event_data)
94
+ event_class = event_data[:event_class]
95
+ event_class.respond_to?(:audit_event?) && event_class.audit_event?
96
+ end
97
+
98
+ # Check if event requires signing
99
+ #
100
+ # Signing is enabled by default for all audit events.
101
+ # Can be disabled via `signing enabled: false` DSL.
102
+ #
103
+ # @param event_data [Hash] Event data
104
+ # @return [Boolean] true if signing required
105
+ def requires_signing?(event_data)
106
+ event_class = event_data[:event_class]
107
+
108
+ # Default: true (sign all audit events unless explicitly disabled)
109
+ return true unless event_class.respond_to?(:signing_enabled?)
110
+
111
+ event_class.signing_enabled?
112
+ end
113
+
114
+ # Sign event with HMAC-SHA256
115
+ #
116
+ # @param event_data [Hash] Event data
117
+ # @return [Hash] Event data with signature
118
+ def sign_event(event_data)
119
+ # 1. Create canonical representation (sorted JSON for consistency)
120
+ canonical = canonical_representation(event_data)
121
+
122
+ # 2. Generate HMAC-SHA256 signature
123
+ signature = generate_signature(canonical)
124
+
125
+ # 3. Add signature metadata
126
+ event_data.merge(
127
+ audit_signature: signature,
128
+ audit_signed_at: Time.now.utc.iso8601(6),
129
+ audit_canonical: canonical
130
+ )
131
+ end
132
+
133
+ # Create canonical representation for signing
134
+ #
135
+ # @param event_data [Hash] Event data
136
+ # @return [String] Canonical JSON string
137
+ def canonical_representation(event_data)
138
+ # Extract fields that should be signed
139
+ signable_data = {
140
+ event_name: event_data[:event_name],
141
+ payload: event_data[:payload],
142
+ timestamp: event_data[:timestamp],
143
+ version: event_data[:version]
144
+ }
145
+
146
+ # Convert to sorted JSON (deterministic)
147
+ JSON.generate(sort_hash(signable_data))
148
+ end
149
+
150
+ # Generate HMAC-SHA256 signature
151
+ #
152
+ # @param data [String] Data to sign
153
+ # @return [String] Hex-encoded signature
154
+ def generate_signature(data)
155
+ OpenSSL::HMAC.hexdigest("SHA256", SIGNING_KEY, data)
156
+ end
157
+
158
+ # Sort hash recursively for deterministic JSON
159
+ #
160
+ # @param obj [Object] Object to sort
161
+ # @return [Object] Sorted object
162
+ def sort_hash(obj)
163
+ case obj
164
+ when Hash
165
+ obj.keys.sort.to_h { |k| [k, sort_hash(obj[k])] }
166
+ when Array
167
+ obj.map { |v| sort_hash(v) }
168
+ else
169
+ obj
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Middleware
5
+ # Base class for all E11y middlewares.
6
+ #
7
+ # Provides the contract for middleware chain pattern and zone-based organization.
8
+ # All middlewares must inherit from this class.
9
+ #
10
+ # @abstract Subclasses must implement {#call} to process events.
11
+ #
12
+ # @example Basic Middleware
13
+ # class MyMiddleware < E11y::Middleware::Base
14
+ # middleware_zone :pre_processing
15
+ #
16
+ # def call(event_data)
17
+ # # Process event
18
+ # event_data[:custom_field] = "value"
19
+ #
20
+ # # Continue chain
21
+ # @app.call(event_data)
22
+ # end
23
+ # end
24
+ #
25
+ # @example Zone-Aware Middleware
26
+ # class SafeEnrichment < E11y::Middleware::Base
27
+ # middleware_zone :pre_processing
28
+ # modifies_fields :metadata, :context
29
+ #
30
+ # def call(event_data)
31
+ # validate_zone_rules!(event_data)
32
+ #
33
+ # event_data[:payload][:metadata] = fetch_metadata
34
+ # @app.call(event_data)
35
+ # end
36
+ # end
37
+ #
38
+ # @see ADR-015 Middleware Execution Order
39
+ # @see ADR-015 §3.4 Middleware Zones & Modification Rules
40
+ class Base
41
+ # Valid middleware zones in execution order
42
+ VALID_ZONES = %i[
43
+ pre_processing
44
+ security
45
+ routing
46
+ post_processing
47
+ adapters
48
+ ].freeze
49
+
50
+ class << self
51
+ # Declare which zone this middleware belongs to.
52
+ #
53
+ # Zones define execution order and modification constraints:
54
+ # - `:pre_processing` - Add fields before PII filtering
55
+ # - `:security` - PII filtering (critical zone)
56
+ # - `:routing` - Rate limiting, sampling (read-only decisions)
57
+ # - `:post_processing` - Add metadata after PII filtering
58
+ # - `:adapters` - Route to buffers and adapters
59
+ #
60
+ # @param zone [Symbol] The zone this middleware belongs to
61
+ # @return [Symbol] The assigned zone
62
+ # @raise [ArgumentError] if zone is not valid
63
+ #
64
+ # @example
65
+ # class MyMiddleware < E11y::Middleware::Base
66
+ # middleware_zone :pre_processing
67
+ # end
68
+ #
69
+ # @see ADR-015 §3.4.2 Middleware Zones
70
+ def middleware_zone(zone = nil)
71
+ if zone
72
+ unless VALID_ZONES.include?(zone)
73
+ raise ArgumentError,
74
+ "Invalid middleware zone: #{zone.inspect}. " \
75
+ "Must be one of #{VALID_ZONES.inspect}"
76
+ end
77
+ @middleware_zone = zone
78
+ end
79
+
80
+ # Return zone (getter if no argument provided)
81
+ @middleware_zone || inherited_zone
82
+ end
83
+
84
+ # Declare which fields this middleware modifies.
85
+ #
86
+ # Used for zone validation and documentation.
87
+ #
88
+ # @param fields [Array<Symbol>] Field names this middleware modifies
89
+ # @return [Array<Symbol>] The declared modified fields
90
+ #
91
+ # @example
92
+ # class MyMiddleware < E11y::Middleware::Base
93
+ # modifies_fields :trace_id, :timestamp
94
+ # end
95
+ def modifies_fields(*fields)
96
+ @modifies_fields = fields if fields.any?
97
+ @modifies_fields || []
98
+ end
99
+
100
+ private
101
+
102
+ # Get zone from parent class if not explicitly set on current class
103
+ # @return [Symbol, nil]
104
+ def inherited_zone
105
+ return nil unless superclass.respond_to?(:middleware_zone, true)
106
+ return nil if superclass == E11y::Middleware::Base
107
+
108
+ superclass.middleware_zone
109
+ end
110
+ end
111
+
112
+ # Initialize middleware with the next middleware in chain.
113
+ #
114
+ # @param app [#call] The next middleware or final endpoint in the chain
115
+ def initialize(app)
116
+ @app = app
117
+ end
118
+
119
+ # Process an event and pass it to the next middleware.
120
+ #
121
+ # @abstract Subclasses must implement this method
122
+ # @param event_data [Hash] The event hash to process
123
+ # @return [void]
124
+ # @raise [NotImplementedError] if not implemented by subclass
125
+ #
126
+ # @example
127
+ # def call(event_data)
128
+ # # Pre-processing
129
+ # event_data[:processed_at] = Time.now.utc
130
+ #
131
+ # # Continue chain
132
+ # @app.call(event_data)
133
+ # end
134
+ def call(_event_data)
135
+ raise NotImplementedError,
136
+ "#{self.class.name} must implement #call(event_data)"
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/middleware/base"
4
+ require "e11y/slo/event_driven"
5
+
6
+ module E11y
7
+ module Middleware
8
+ # EventSLO Middleware for Event-Driven SLO tracking (ADR-014).
9
+ #
10
+ # Automatically processes events with SLO configuration enabled,
11
+ # computes `slo_status` from payload, and emits metrics.
12
+ #
13
+ # **Features:**
14
+ # - Auto-detects events with `slo { enabled true }`
15
+ # - Calls `slo_status_from` proc to compute 'success'/'failure'
16
+ # - Emits `slo_event_result_total{slo_status}` metric to Yabeda
17
+ # - Never fails event tracking (graceful error handling)
18
+ #
19
+ # **Middleware Zone:** `:post_processing` (after routing, before adapters)
20
+ #
21
+ # **ADR References:**
22
+ # - ADR-014 §3 (Event SLO DSL)
23
+ # - ADR-014 §4 (SLO Status Calculation)
24
+ # - ADR-014 §6 (Metrics Export)
25
+ # - ADR-015 §3 (Middleware Order)
26
+ #
27
+ # **Use Case:** UC-014 (Event-Driven SLO)
28
+ #
29
+ # @example Configuration
30
+ # E11y.configure do |config|
31
+ # # Enable EventSLO middleware (auto-enabled if any Events have slo { enabled true })
32
+ # config.pipeline.use E11y::Middleware::EventSlo, zone: :post_processing
33
+ # end
34
+ #
35
+ # @example Event with SLO
36
+ # module Events
37
+ # class PaymentProcessed < E11y::Event::Base
38
+ # schema do
39
+ # required(:payment_id).filled(:string)
40
+ # required(:status).filled(:string)
41
+ # end
42
+ #
43
+ # slo do
44
+ # enabled true
45
+ # slo_status_from do |payload|
46
+ # case payload[:status]
47
+ # when 'completed' then 'success'
48
+ # when 'failed' then 'failure'
49
+ # else nil # Not counted
50
+ # end
51
+ # end
52
+ # end
53
+ # end
54
+ # end
55
+ #
56
+ # # Tracking will automatically emit SLO metric:
57
+ # Events::PaymentProcessed.track(payment_id: 'p123', status: 'completed')
58
+ # # → Emits: slo_event_result_total{event_name="payment.processed", slo_status="success"} +1
59
+ #
60
+ # @see ADR-014 for complete Event-Driven SLO architecture
61
+ class EventSlo < Base
62
+ middleware_zone :post_processing
63
+
64
+ # Process event and emit SLO metric if SLO is enabled.
65
+ #
66
+ # @param event_data [Hash] Event payload
67
+ # @return [Hash] Unchanged event_data (passthrough)
68
+ def call(event_data)
69
+ # Skip if SLO not enabled for this event
70
+ # Support explicit event_class (for testing) or resolve from event_name
71
+ event_class = event_data[:event_class] || resolve_event_class(event_data)
72
+ return event_data unless event_class.respond_to?(:slo_config)
73
+ return event_data unless event_class.slo_config&.enabled?
74
+
75
+ # Compute slo_status from payload
76
+ slo_status = compute_slo_status(event_class, event_data[:payload])
77
+ return event_data unless slo_status
78
+
79
+ # Emit SLO metric
80
+ emit_slo_metric(event_class, slo_status, event_data[:payload])
81
+
82
+ event_data # Passthrough (never modify event_data)
83
+ rescue StandardError => e
84
+ # Never fail event tracking due to SLO processing
85
+ E11y.logger.error(
86
+ "[E11y::Middleware::EventSlo] SLO processing failed for #{event_data[:event_name]}: #{e.message}"
87
+ )
88
+ event_data
89
+ end
90
+
91
+ private
92
+
93
+ # Resolve Event class from event_name.
94
+ #
95
+ # @param event_data [Hash] Event payload
96
+ # @return [Class, nil] Event class or nil if not found
97
+ def resolve_event_class(event_data)
98
+ event_name = event_data[:event_name]
99
+ return nil unless event_name
100
+
101
+ # Convert event_name to class name (e.g., "payment.processed" → "Events::PaymentProcessed")
102
+ # This assumes Rails autoloading or explicit requires
103
+ class_name = event_name.to_s.split(".").map(&:capitalize).join
104
+ "Events::#{class_name}".constantize
105
+ rescue NameError
106
+ # Event class not found (may be from external source)
107
+ nil
108
+ end
109
+
110
+ # Compute slo_status using event's slo_status_from proc.
111
+ #
112
+ # @param event_class [Class] Event class
113
+ # @param payload [Hash] Event payload
114
+ # @return [String, nil] 'success', 'failure', or nil
115
+ def compute_slo_status(event_class, payload)
116
+ return nil unless event_class.slo_config.slo_status_proc
117
+
118
+ event_class.slo_config.slo_status_proc.call(payload)
119
+ rescue StandardError => e
120
+ E11y.logger.error(
121
+ "[E11y::Middleware::EventSlo] Failed to compute slo_status for #{event_class.name}: #{e.message}"
122
+ )
123
+ nil
124
+ end
125
+
126
+ # Emit SLO metric to Yabeda/Prometheus.
127
+ #
128
+ # @param event_class [Class] Event class
129
+ # @param slo_status [String] 'success' or 'failure'
130
+ # @param payload [Hash] Event payload
131
+ # @return [void]
132
+ def emit_slo_metric(event_class, slo_status, payload)
133
+ labels = build_slo_labels(event_class, slo_status, payload)
134
+
135
+ E11y::Metrics.increment(:slo_event_result_total, labels)
136
+ rescue StandardError => e
137
+ E11y.logger.error(
138
+ "[E11y::Middleware::EventSlo] Failed to emit SLO metric for #{event_class.name}: #{e.message}"
139
+ )
140
+ end
141
+
142
+ # Build metric labels for SLO.
143
+ #
144
+ # @param event_class [Class] Event class
145
+ # @param slo_status [String] 'success' or 'failure'
146
+ # @param payload [Hash] Event payload
147
+ # @return [Hash] Metric labels
148
+ def build_slo_labels(event_class, slo_status, payload)
149
+ labels = {
150
+ event_name: event_class.event_name,
151
+ slo_status: slo_status
152
+ }
153
+
154
+ # Add custom SLO name if configured
155
+ labels[:slo_name] = event_class.slo_config.contributes_to_value if event_class.slo_config.contributes_to_value
156
+
157
+ # Add group_by field if configured
158
+ if event_class.slo_config.group_by_field
159
+ field = event_class.slo_config.group_by_field
160
+ labels[:group_by] = payload[field].to_s if payload[field]
161
+ end
162
+
163
+ labels
164
+ end
165
+ end
166
+ end
167
+ end