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.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +69 -0
- data/CHANGELOG.md +26 -0
- data/CODE_OF_CONDUCT.md +64 -0
- data/LICENSE.txt +21 -0
- data/README.md +179 -0
- data/Rakefile +37 -0
- data/benchmarks/run_all.rb +33 -0
- data/config/README.md +83 -0
- data/config/loki-local-config.yaml +35 -0
- data/config/prometheus.yml +15 -0
- data/docker-compose.yml +78 -0
- data/docs/00-ICP-AND-TIMELINE.md +483 -0
- data/docs/01-SCALE-REQUIREMENTS.md +858 -0
- data/docs/ADR-001-architecture.md +2617 -0
- data/docs/ADR-002-metrics-yabeda.md +1395 -0
- data/docs/ADR-003-slo-observability.md +3337 -0
- data/docs/ADR-004-adapter-architecture.md +2385 -0
- data/docs/ADR-005-tracing-context.md +1372 -0
- data/docs/ADR-006-security-compliance.md +4143 -0
- data/docs/ADR-007-opentelemetry-integration.md +1385 -0
- data/docs/ADR-008-rails-integration.md +1911 -0
- data/docs/ADR-009-cost-optimization.md +2993 -0
- data/docs/ADR-010-developer-experience.md +2166 -0
- data/docs/ADR-011-testing-strategy.md +1836 -0
- data/docs/ADR-012-event-evolution.md +958 -0
- data/docs/ADR-013-reliability-error-handling.md +2750 -0
- data/docs/ADR-014-event-driven-slo.md +1533 -0
- data/docs/ADR-015-middleware-order.md +1061 -0
- data/docs/ADR-016-self-monitoring-slo.md +1234 -0
- data/docs/API-REFERENCE-L28.md +914 -0
- data/docs/COMPREHENSIVE-CONFIGURATION.md +2366 -0
- data/docs/IMPLEMENTATION_NOTES.md +2804 -0
- data/docs/IMPLEMENTATION_PLAN.md +1971 -0
- data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +586 -0
- data/docs/PLAN.md +148 -0
- data/docs/QUICK-START.md +934 -0
- data/docs/README.md +296 -0
- data/docs/design/00-memory-optimization.md +593 -0
- data/docs/guides/MIGRATION-L27-L28.md +692 -0
- data/docs/guides/PERFORMANCE-BENCHMARKS.md +434 -0
- data/docs/guides/README.md +44 -0
- data/docs/prd/01-overview-vision.md +440 -0
- data/docs/use_cases/README.md +119 -0
- data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +813 -0
- data/docs/use_cases/UC-002-business-event-tracking.md +1953 -0
- data/docs/use_cases/UC-003-pattern-based-metrics.md +1627 -0
- data/docs/use_cases/UC-004-zero-config-slo-tracking.md +728 -0
- data/docs/use_cases/UC-005-sentry-integration.md +759 -0
- data/docs/use_cases/UC-006-trace-context-management.md +905 -0
- data/docs/use_cases/UC-007-pii-filtering.md +2648 -0
- data/docs/use_cases/UC-008-opentelemetry-integration.md +1153 -0
- data/docs/use_cases/UC-009-multi-service-tracing.md +1043 -0
- data/docs/use_cases/UC-010-background-job-tracking.md +1018 -0
- data/docs/use_cases/UC-011-rate-limiting.md +1906 -0
- data/docs/use_cases/UC-012-audit-trail.md +2301 -0
- data/docs/use_cases/UC-013-high-cardinality-protection.md +2127 -0
- data/docs/use_cases/UC-014-adaptive-sampling.md +1940 -0
- data/docs/use_cases/UC-015-cost-optimization.md +735 -0
- data/docs/use_cases/UC-016-rails-logger-migration.md +785 -0
- data/docs/use_cases/UC-017-local-development.md +867 -0
- data/docs/use_cases/UC-018-testing-events.md +1081 -0
- data/docs/use_cases/UC-019-tiered-storage-migration.md +562 -0
- data/docs/use_cases/UC-020-event-versioning.md +708 -0
- data/docs/use_cases/UC-021-error-handling-retry-dlq.md +956 -0
- data/docs/use_cases/UC-022-event-registry.md +648 -0
- data/docs/use_cases/backlog.md +226 -0
- data/e11y.gemspec +76 -0
- data/lib/e11y/adapters/adaptive_batcher.rb +207 -0
- data/lib/e11y/adapters/audit_encrypted.rb +239 -0
- data/lib/e11y/adapters/base.rb +580 -0
- data/lib/e11y/adapters/file.rb +224 -0
- data/lib/e11y/adapters/in_memory.rb +216 -0
- data/lib/e11y/adapters/loki.rb +333 -0
- data/lib/e11y/adapters/otel_logs.rb +203 -0
- data/lib/e11y/adapters/registry.rb +141 -0
- data/lib/e11y/adapters/sentry.rb +230 -0
- data/lib/e11y/adapters/stdout.rb +108 -0
- data/lib/e11y/adapters/yabeda.rb +370 -0
- data/lib/e11y/buffers/adaptive_buffer.rb +339 -0
- data/lib/e11y/buffers/base_buffer.rb +40 -0
- data/lib/e11y/buffers/request_scoped_buffer.rb +246 -0
- data/lib/e11y/buffers/ring_buffer.rb +267 -0
- data/lib/e11y/buffers.rb +14 -0
- data/lib/e11y/console.rb +122 -0
- data/lib/e11y/current.rb +48 -0
- data/lib/e11y/event/base.rb +894 -0
- data/lib/e11y/event/value_sampling_config.rb +84 -0
- data/lib/e11y/events/base_audit_event.rb +43 -0
- data/lib/e11y/events/base_payment_event.rb +33 -0
- data/lib/e11y/events/rails/cache/delete.rb +21 -0
- data/lib/e11y/events/rails/cache/read.rb +23 -0
- data/lib/e11y/events/rails/cache/write.rb +22 -0
- data/lib/e11y/events/rails/database/query.rb +45 -0
- data/lib/e11y/events/rails/http/redirect.rb +21 -0
- data/lib/e11y/events/rails/http/request.rb +26 -0
- data/lib/e11y/events/rails/http/send_file.rb +21 -0
- data/lib/e11y/events/rails/http/start_processing.rb +26 -0
- data/lib/e11y/events/rails/job/completed.rb +22 -0
- data/lib/e11y/events/rails/job/enqueued.rb +22 -0
- data/lib/e11y/events/rails/job/failed.rb +22 -0
- data/lib/e11y/events/rails/job/scheduled.rb +23 -0
- data/lib/e11y/events/rails/job/started.rb +22 -0
- data/lib/e11y/events/rails/log.rb +56 -0
- data/lib/e11y/events/rails/view/render.rb +23 -0
- data/lib/e11y/events.rb +18 -0
- data/lib/e11y/instruments/active_job.rb +201 -0
- data/lib/e11y/instruments/rails_instrumentation.rb +141 -0
- data/lib/e11y/instruments/sidekiq.rb +175 -0
- data/lib/e11y/logger/bridge.rb +205 -0
- data/lib/e11y/metrics/cardinality_protection.rb +172 -0
- data/lib/e11y/metrics/cardinality_tracker.rb +134 -0
- data/lib/e11y/metrics/registry.rb +234 -0
- data/lib/e11y/metrics/relabeling.rb +226 -0
- data/lib/e11y/metrics.rb +102 -0
- data/lib/e11y/middleware/audit_signing.rb +174 -0
- data/lib/e11y/middleware/base.rb +140 -0
- data/lib/e11y/middleware/event_slo.rb +167 -0
- data/lib/e11y/middleware/pii_filter.rb +266 -0
- data/lib/e11y/middleware/pii_filtering.rb +280 -0
- data/lib/e11y/middleware/rate_limiting.rb +214 -0
- data/lib/e11y/middleware/request.rb +163 -0
- data/lib/e11y/middleware/routing.rb +157 -0
- data/lib/e11y/middleware/sampling.rb +254 -0
- data/lib/e11y/middleware/slo.rb +168 -0
- data/lib/e11y/middleware/trace_context.rb +131 -0
- data/lib/e11y/middleware/validation.rb +118 -0
- data/lib/e11y/middleware/versioning.rb +132 -0
- data/lib/e11y/middleware.rb +12 -0
- data/lib/e11y/pii/patterns.rb +90 -0
- data/lib/e11y/pii.rb +13 -0
- data/lib/e11y/pipeline/builder.rb +155 -0
- data/lib/e11y/pipeline/zone_validator.rb +110 -0
- data/lib/e11y/pipeline.rb +12 -0
- data/lib/e11y/presets/audit_event.rb +65 -0
- data/lib/e11y/presets/debug_event.rb +34 -0
- data/lib/e11y/presets/high_value_event.rb +51 -0
- data/lib/e11y/presets.rb +19 -0
- data/lib/e11y/railtie.rb +138 -0
- data/lib/e11y/reliability/circuit_breaker.rb +216 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +277 -0
- data/lib/e11y/reliability/dlq/filter.rb +117 -0
- data/lib/e11y/reliability/retry_handler.rb +207 -0
- data/lib/e11y/reliability/retry_rate_limiter.rb +117 -0
- data/lib/e11y/sampling/error_spike_detector.rb +225 -0
- data/lib/e11y/sampling/load_monitor.rb +161 -0
- data/lib/e11y/sampling/stratified_tracker.rb +92 -0
- data/lib/e11y/sampling/value_extractor.rb +82 -0
- data/lib/e11y/self_monitoring/buffer_monitor.rb +79 -0
- data/lib/e11y/self_monitoring/performance_monitor.rb +97 -0
- data/lib/e11y/self_monitoring/reliability_monitor.rb +146 -0
- data/lib/e11y/slo/event_driven.rb +150 -0
- data/lib/e11y/slo/tracker.rb +119 -0
- data/lib/e11y/version.rb +9 -0
- data/lib/e11y.rb +283 -0
- 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
|