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,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# Versioning Middleware (ADR-012, UC-020)
|
|
6
|
+
#
|
|
7
|
+
# Extracts version from event class name and adds `v:` field to payload.
|
|
8
|
+
# Only adds `v:` if version > 1 (reduces noise for V1 events).
|
|
9
|
+
#
|
|
10
|
+
# **Features:**
|
|
11
|
+
# - Extracts version from class name (e.g., `Events::OrderPaidV2` → `v: 2`)
|
|
12
|
+
# - Normalizes event_name (removes version suffix for consistent queries)
|
|
13
|
+
# - Only adds `v:` field if version > 1
|
|
14
|
+
# - Opt-in (must be explicitly enabled)
|
|
15
|
+
#
|
|
16
|
+
# **ADR References:**
|
|
17
|
+
# - ADR-012 §2 (Parallel Versions)
|
|
18
|
+
# - ADR-012 §3 (Naming Convention)
|
|
19
|
+
# - ADR-012 §4 (Version in Payload)
|
|
20
|
+
# - ADR-015 §3 (Middleware Order - Versioning in :pre_processing zone)
|
|
21
|
+
#
|
|
22
|
+
# **Use Case:** UC-020 (Event Versioning)
|
|
23
|
+
#
|
|
24
|
+
# @example Configuration
|
|
25
|
+
# E11y.configure do |config|
|
|
26
|
+
# # Enable versioning middleware (opt-in)
|
|
27
|
+
# config.pipeline.use E11y::Middleware::Versioning
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# @example V1 Event (no version in payload)
|
|
31
|
+
# class Events::OrderPaid < E11y::Event::Base
|
|
32
|
+
# # No version suffix → V1 (implicit)
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # Result payload:
|
|
36
|
+
# {
|
|
37
|
+
# event_name: "order.paid", # Normalized (no version)
|
|
38
|
+
# # No `v:` field (V1 is implicit)
|
|
39
|
+
# payload: { ... }
|
|
40
|
+
# }
|
|
41
|
+
#
|
|
42
|
+
# @example V2 Event (version in payload)
|
|
43
|
+
# class Events::OrderPaidV2 < E11y::Event::Base
|
|
44
|
+
# # Version suffix → V2
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# # Result payload:
|
|
48
|
+
# {
|
|
49
|
+
# event_name: "order.paid", # Normalized (no version)
|
|
50
|
+
# v: 2, # Version extracted from class name
|
|
51
|
+
# payload: { ... }
|
|
52
|
+
# }
|
|
53
|
+
#
|
|
54
|
+
# @see ADR-012 for versioning architecture
|
|
55
|
+
# @see UC-020 for use cases
|
|
56
|
+
class Versioning < Base
|
|
57
|
+
# Version extraction regex (matches V2, V3, etc. at end of class name)
|
|
58
|
+
VERSION_REGEX = /V(\d+)$/
|
|
59
|
+
|
|
60
|
+
# Process event and add version field if needed
|
|
61
|
+
#
|
|
62
|
+
# @param event_data [Hash] Event payload
|
|
63
|
+
# @return [Hash] Event data with version field (if > 1)
|
|
64
|
+
def call(event_data)
|
|
65
|
+
# Extract version from event_name (class name)
|
|
66
|
+
version = extract_version(event_data[:event_name])
|
|
67
|
+
|
|
68
|
+
# Add version field only if > 1 (ADR-012 §4.2)
|
|
69
|
+
event_data[:v] = version if version > 1
|
|
70
|
+
|
|
71
|
+
# Normalize event_name (remove version suffix for consistent queries)
|
|
72
|
+
event_data[:event_name] = normalize_event_name(event_data[:event_name])
|
|
73
|
+
|
|
74
|
+
event_data
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Extract version from event class name
|
|
80
|
+
#
|
|
81
|
+
# @param class_name [String] Event class name (e.g., "Events::OrderPaidV2")
|
|
82
|
+
# @return [Integer] Version number (default: 1)
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# extract_version("Events::OrderPaid") => 1
|
|
86
|
+
# extract_version("Events::OrderPaidV2") => 2
|
|
87
|
+
# extract_version("Events::OrderPaidV3") => 3
|
|
88
|
+
def extract_version(class_name)
|
|
89
|
+
return 1 unless class_name
|
|
90
|
+
|
|
91
|
+
# Extract version from class name (e.g., "Events::OrderPaidV2" → 2)
|
|
92
|
+
match = class_name.match(VERSION_REGEX)
|
|
93
|
+
match ? match[1].to_i : 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Normalize event_name by removing version suffix
|
|
97
|
+
#
|
|
98
|
+
# This ensures consistent querying across versions:
|
|
99
|
+
# - "Events::OrderPaid" → "order.paid"
|
|
100
|
+
# - "Events::OrderPaidV2" → "order.paid" (same name!)
|
|
101
|
+
#
|
|
102
|
+
# **Rationale (ADR-012 §3.2):**
|
|
103
|
+
# Query: `WHERE event_name = 'order.paid'` matches ALL versions
|
|
104
|
+
#
|
|
105
|
+
# @param class_name [String] Event class name
|
|
106
|
+
# @return [String] Normalized event name (snake_case, no version)
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# normalize_event_name("Events::OrderPaid") => "order.paid"
|
|
110
|
+
# normalize_event_name("Events::OrderPaidV2") => "order.paid"
|
|
111
|
+
# normalize_event_name("Events::OrderPaidV3") => "order.paid"
|
|
112
|
+
def normalize_event_name(class_name)
|
|
113
|
+
return class_name unless class_name
|
|
114
|
+
|
|
115
|
+
# Remove "Events::" namespace prefix
|
|
116
|
+
name = class_name.sub(/^Events::/, "")
|
|
117
|
+
|
|
118
|
+
# Remove version suffix (V2, V3, etc.)
|
|
119
|
+
name = name.sub(VERSION_REGEX, "")
|
|
120
|
+
|
|
121
|
+
# Convert nested namespaces to dots first
|
|
122
|
+
name = name.gsub("::", ".")
|
|
123
|
+
|
|
124
|
+
# Convert to snake_case
|
|
125
|
+
name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') # ABCWord → ABC_Word
|
|
126
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2') # wordWord → word_Word
|
|
127
|
+
.downcase
|
|
128
|
+
.gsub("_", ".") # Convert underscores to dots for event names
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
# Middleware pipeline for event processing.
|
|
5
|
+
#
|
|
6
|
+
# Provides zone-based middleware architecture with clear modification rules.
|
|
7
|
+
#
|
|
8
|
+
# @see E11y::Middleware::Base
|
|
9
|
+
# @see ADR-015 Middleware Execution Order
|
|
10
|
+
module Middleware
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module PII
|
|
5
|
+
# Universal PII patterns for automatic detection
|
|
6
|
+
#
|
|
7
|
+
# Patterns are used by PIIFiltering middleware to automatically detect
|
|
8
|
+
# and mask/hash sensitive data in event payloads.
|
|
9
|
+
#
|
|
10
|
+
# @see E11y::Middleware::PIIFiltering
|
|
11
|
+
# @see ADR-006 PII Security
|
|
12
|
+
# @see UC-007 PII Filtering
|
|
13
|
+
module Patterns
|
|
14
|
+
# Email pattern (RFC 5322 simplified)
|
|
15
|
+
EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/
|
|
16
|
+
|
|
17
|
+
# Password-like field names
|
|
18
|
+
PASSWORD_FIELDS = /password|passwd|pwd|secret|token|api[_-]?key/i
|
|
19
|
+
|
|
20
|
+
# Social Security Number (US format: XXX-XX-XXXX)
|
|
21
|
+
SSN = /\b\d{3}-\d{2}-\d{4}\b/
|
|
22
|
+
|
|
23
|
+
# Credit card number (Visa, MC, Amex, Discover)
|
|
24
|
+
# Luhn algorithm validation not included (performance trade-off)
|
|
25
|
+
CREDIT_CARD = /\b(?:\d{4}[- ]?){3}\d{4}\b/
|
|
26
|
+
|
|
27
|
+
# IPv4 address
|
|
28
|
+
IPV4 = /\b(?:\d{1,3}\.){3}\d{1,3}\b/
|
|
29
|
+
|
|
30
|
+
# Phone number (various formats)
|
|
31
|
+
PHONE = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/
|
|
32
|
+
|
|
33
|
+
# All patterns combined for bulk detection
|
|
34
|
+
ALL = [
|
|
35
|
+
EMAIL,
|
|
36
|
+
PASSWORD_FIELDS,
|
|
37
|
+
SSN,
|
|
38
|
+
CREDIT_CARD,
|
|
39
|
+
IPV4,
|
|
40
|
+
PHONE
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Field name patterns that indicate PII
|
|
44
|
+
# Used for field-level detection (case-insensitive)
|
|
45
|
+
FIELD_PATTERNS = {
|
|
46
|
+
email: /email|e[_-]?mail/i,
|
|
47
|
+
password: /password|passwd|pwd|secret|token|api[_-]?key/i,
|
|
48
|
+
ssn: /ssn|social[_-]?security|tax[_-]?id/i,
|
|
49
|
+
credit_card: /card|cc[_-]?number|credit[_-]?card|pan/i,
|
|
50
|
+
phone: /phone|mobile|tel|telephone/i,
|
|
51
|
+
ip: /\Aip\z|ip[_-]?address|remote[_-]?addr/i,
|
|
52
|
+
address: /\Aaddress\z|street|city|zip|postal/i,
|
|
53
|
+
name: /name|first[_-]?name|last[_-]?name|full[_-]?name/i,
|
|
54
|
+
dob: /birth|dob|date[_-]?of[_-]?birth/i
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
# Check if field name matches PII pattern
|
|
58
|
+
#
|
|
59
|
+
# @param field_name [String, Symbol] Field name to check
|
|
60
|
+
# @return [Symbol, nil] PII type if matched, nil otherwise
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# Patterns.detect_field_type(:email) # => :email
|
|
64
|
+
# Patterns.detect_field_type(:user_email) # => :email
|
|
65
|
+
# Patterns.detect_field_type(:id) # => nil
|
|
66
|
+
def self.detect_field_type(field_name)
|
|
67
|
+
field_str = field_name.to_s
|
|
68
|
+
FIELD_PATTERNS.each do |type, pattern|
|
|
69
|
+
return type if field_str.match?(pattern)
|
|
70
|
+
end
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if value matches any PII pattern
|
|
75
|
+
#
|
|
76
|
+
# @param value [String] Value to check
|
|
77
|
+
# @return [Boolean] true if PII detected
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# Patterns.contains_pii?("user@example.com") # => true
|
|
81
|
+
# Patterns.contains_pii?("123-45-6789") # => true
|
|
82
|
+
# Patterns.contains_pii?("hello world") # => false
|
|
83
|
+
def self.contains_pii?(value)
|
|
84
|
+
return false unless value.is_a?(String)
|
|
85
|
+
|
|
86
|
+
ALL.any? { |pattern| value.match?(pattern) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/e11y/pii.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# E11y::PII module - PII detection and filtering utilities
|
|
4
|
+
#
|
|
5
|
+
# This module provides PII pattern detection and filtering strategies
|
|
6
|
+
# for the PIIFiltering middleware.
|
|
7
|
+
#
|
|
8
|
+
# @see E11y::PII::Patterns
|
|
9
|
+
# @see E11y::Middleware::PIIFiltering
|
|
10
|
+
module E11y
|
|
11
|
+
module PII
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Builder for configuring and validating middleware pipelines.
|
|
6
|
+
#
|
|
7
|
+
# Provides zone-based middleware organization with boot-time validation
|
|
8
|
+
# to ensure correct execution order per ADR-015 §3.4.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic Pipeline Configuration
|
|
11
|
+
# builder = E11y::Pipeline::Builder.new
|
|
12
|
+
#
|
|
13
|
+
# builder.use E11y::Middleware::TraceContext
|
|
14
|
+
# builder.use E11y::Middleware::Validation
|
|
15
|
+
# builder.use E11y::Middleware::PIIFiltering
|
|
16
|
+
#
|
|
17
|
+
# builder.validate_zones! # Boot-time validation
|
|
18
|
+
#
|
|
19
|
+
# pipeline = builder.build(final_app)
|
|
20
|
+
# pipeline.call(event_data)
|
|
21
|
+
#
|
|
22
|
+
# @example Zone-Based Configuration
|
|
23
|
+
# builder.zone(:pre_processing) do
|
|
24
|
+
# use E11y::Middleware::TraceContext
|
|
25
|
+
# use E11y::Middleware::Validation
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# builder.zone(:security) do
|
|
29
|
+
# use E11y::Middleware::PIIFiltering
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# @see E11y::Middleware::Base
|
|
33
|
+
# @see ADR-015 §3.4 Middleware Zones & Modification Rules
|
|
34
|
+
class Builder
|
|
35
|
+
# Middleware entry: [middleware_class, args, options]
|
|
36
|
+
MiddlewareEntry = Struct.new(:middleware_class, :args, :options, keyword_init: true)
|
|
37
|
+
|
|
38
|
+
# @return [Array<MiddlewareEntry>] Registered middlewares
|
|
39
|
+
attr_reader :middlewares
|
|
40
|
+
|
|
41
|
+
def initialize
|
|
42
|
+
@middlewares = []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Add a middleware to the pipeline.
|
|
46
|
+
#
|
|
47
|
+
# @param middleware_class [Class] Middleware class (must inherit from Base)
|
|
48
|
+
# @param args [Array] Positional arguments for middleware constructor
|
|
49
|
+
# @param options [Hash] Keyword arguments for middleware constructor
|
|
50
|
+
# @return [self] For method chaining
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# builder.use E11y::Middleware::TraceContext
|
|
54
|
+
# builder.use E11y::Middleware::RateLimiting, limit: 1000
|
|
55
|
+
#
|
|
56
|
+
# @see ADR-015 Pipeline Flow
|
|
57
|
+
def use(middleware_class, *args, **options)
|
|
58
|
+
unless middleware_class < E11y::Middleware::Base
|
|
59
|
+
raise ArgumentError,
|
|
60
|
+
"Middleware #{middleware_class} must inherit from E11y::Middleware::Base"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@middlewares << MiddlewareEntry.new(
|
|
64
|
+
middleware_class: middleware_class,
|
|
65
|
+
args: args,
|
|
66
|
+
options: options
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Configure middlewares within a specific zone.
|
|
73
|
+
#
|
|
74
|
+
# This is a convenience method for organizing middleware configuration.
|
|
75
|
+
# Zone validation happens at boot-time via {#validate_zones!}.
|
|
76
|
+
#
|
|
77
|
+
# @param zone [Symbol] The zone name (must be valid per Middleware::Base::VALID_ZONES)
|
|
78
|
+
# @yield Block for configuring middlewares in this zone (executed in builder context)
|
|
79
|
+
# @return [self] For method chaining
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# builder.zone(:security) do
|
|
83
|
+
# use E11y::Middleware::PIIFiltering
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# @see ADR-015 §3.4.2 Middleware Zones
|
|
87
|
+
def zone(zone, &block)
|
|
88
|
+
unless E11y::Middleware::Base::VALID_ZONES.include?(zone)
|
|
89
|
+
raise ArgumentError,
|
|
90
|
+
"Invalid zone: #{zone.inspect}. " \
|
|
91
|
+
"Must be one of #{E11y::Middleware::Base::VALID_ZONES.inspect}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
instance_eval(&block) if block
|
|
95
|
+
self
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Build the middleware pipeline.
|
|
99
|
+
#
|
|
100
|
+
# Constructs a chain of middleware instances, passing each middleware
|
|
101
|
+
# to the next one in reverse order (Rack pattern).
|
|
102
|
+
#
|
|
103
|
+
# @param app [#call] The final application/endpoint in the chain
|
|
104
|
+
# @return [#call] The complete middleware pipeline
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# pipeline = builder.build(final_app)
|
|
108
|
+
# result = pipeline.call(event_data)
|
|
109
|
+
def build(app)
|
|
110
|
+
@middlewares.reverse.reduce(app) do |next_app, entry|
|
|
111
|
+
entry.middleware_class.new(next_app, *entry.args, **entry.options)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Validate middleware zone ordering at boot time.
|
|
116
|
+
#
|
|
117
|
+
# Ensures middlewares are ordered correctly according to their declared zones.
|
|
118
|
+
# This prevents zone violations like PII filtering running after custom middleware.
|
|
119
|
+
#
|
|
120
|
+
# Delegates to {E11y::Pipeline::ZoneValidator} for validation logic.
|
|
121
|
+
#
|
|
122
|
+
# @return [void]
|
|
123
|
+
# @raise [E11y::InvalidPipelineError] if zone ordering is invalid
|
|
124
|
+
#
|
|
125
|
+
# @example Boot-time validation
|
|
126
|
+
# Rails.application.config.after_initialize do
|
|
127
|
+
# E11y.pipeline_builder.validate_zones!
|
|
128
|
+
# end
|
|
129
|
+
#
|
|
130
|
+
# @see E11y::Pipeline::ZoneValidator
|
|
131
|
+
# @see ADR-015 §3.4.5 Zone Validation
|
|
132
|
+
def validate_zones!
|
|
133
|
+
validator = E11y::Pipeline::ZoneValidator.new(@middlewares)
|
|
134
|
+
validator.validate_boot_time!
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Clear all registered middlewares.
|
|
138
|
+
#
|
|
139
|
+
# @return [void]
|
|
140
|
+
def clear
|
|
141
|
+
@middlewares.clear
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Get numeric index for a zone (for ordering validation)
|
|
147
|
+
#
|
|
148
|
+
# @param zone [Symbol] Zone name
|
|
149
|
+
# @return [Integer] Zone index (0-4)
|
|
150
|
+
def zone_index(zone)
|
|
151
|
+
E11y::Middleware::Base::VALID_ZONES.index(zone) || -1
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Pipeline
|
|
5
|
+
# Validator for middleware zone rules and constraints.
|
|
6
|
+
#
|
|
7
|
+
# Provides boot-time validation to ensure middleware zones are correctly
|
|
8
|
+
# ordered and PII bypass is prevented.
|
|
9
|
+
#
|
|
10
|
+
# **Design Decision:** Only boot-time validation is performed.
|
|
11
|
+
# Runtime validation was deemed unnecessary as:
|
|
12
|
+
# - Boot-time validation catches all configuration errors
|
|
13
|
+
# - Runtime validation adds ~1ms overhead per event
|
|
14
|
+
# - Pipeline configuration is static after boot
|
|
15
|
+
#
|
|
16
|
+
# @see ADR-015 §3.4.5 Zone Validation
|
|
17
|
+
# @see ADR-015 §3.4.4 Custom Middleware Constraints
|
|
18
|
+
class ZoneValidator
|
|
19
|
+
# Error raised when zone ordering is invalid
|
|
20
|
+
class ZoneOrderError < E11y::InvalidPipelineError; end
|
|
21
|
+
|
|
22
|
+
# Zone ordering constraints (valid transitions)
|
|
23
|
+
ZONE_ORDER = %i[
|
|
24
|
+
pre_processing
|
|
25
|
+
security
|
|
26
|
+
routing
|
|
27
|
+
post_processing
|
|
28
|
+
adapters
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# @param middlewares [Array<MiddlewareEntry>] Middleware entries to validate
|
|
32
|
+
def initialize(middlewares)
|
|
33
|
+
@middlewares = middlewares
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Validate zone ordering at boot time.
|
|
37
|
+
#
|
|
38
|
+
# Ensures middlewares are ordered correctly according to their declared zones.
|
|
39
|
+
# This is a comprehensive check that runs once during application boot.
|
|
40
|
+
#
|
|
41
|
+
# @return [void]
|
|
42
|
+
# @raise [ZoneOrderError] if zone ordering is invalid
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# validator = ZoneValidator.new(pipeline.middlewares)
|
|
46
|
+
# validator.validate_boot_time!
|
|
47
|
+
def validate_boot_time!
|
|
48
|
+
return if @middlewares.empty?
|
|
49
|
+
|
|
50
|
+
previous_zone_index = -1
|
|
51
|
+
|
|
52
|
+
@middlewares.each_with_index do |entry, index|
|
|
53
|
+
middleware_zone = entry.middleware_class.middleware_zone
|
|
54
|
+
|
|
55
|
+
# Skip middlewares without declared zone (optional)
|
|
56
|
+
next unless middleware_zone
|
|
57
|
+
|
|
58
|
+
current_zone_index = zone_index(middleware_zone)
|
|
59
|
+
|
|
60
|
+
# Validate zone progression (must be non-decreasing)
|
|
61
|
+
if current_zone_index < previous_zone_index
|
|
62
|
+
previous_entry = @middlewares[index - 1]
|
|
63
|
+
previous_zone = previous_entry.middleware_class.middleware_zone
|
|
64
|
+
|
|
65
|
+
raise ZoneOrderError,
|
|
66
|
+
build_zone_order_error(entry.middleware_class, middleware_zone,
|
|
67
|
+
previous_entry.middleware_class, previous_zone)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
previous_zone_index = current_zone_index
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Get numeric index for a zone (for ordering validation)
|
|
77
|
+
#
|
|
78
|
+
# @param zone [Symbol] Zone name
|
|
79
|
+
# @return [Integer] Zone index (0-4)
|
|
80
|
+
def zone_index(zone)
|
|
81
|
+
ZONE_ORDER.index(zone) || -1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Build detailed error message for zone order violations
|
|
85
|
+
#
|
|
86
|
+
# @param current_middleware [Class] Current middleware class
|
|
87
|
+
# @param current_zone [Symbol] Current middleware zone
|
|
88
|
+
# @param previous_middleware [Class] Previous middleware class
|
|
89
|
+
# @param previous_zone [Symbol] Previous middleware zone
|
|
90
|
+
# @return [String] Formatted error message
|
|
91
|
+
def build_zone_order_error(current_middleware, current_zone,
|
|
92
|
+
previous_middleware, previous_zone)
|
|
93
|
+
<<~ERROR
|
|
94
|
+
Invalid middleware zone order detected:
|
|
95
|
+
|
|
96
|
+
#{current_middleware.name} (zone: #{current_zone})
|
|
97
|
+
cannot follow
|
|
98
|
+
#{previous_middleware.name} (zone: #{previous_zone})
|
|
99
|
+
|
|
100
|
+
Valid zone order: #{ZONE_ORDER.join(' → ')}
|
|
101
|
+
|
|
102
|
+
This violation prevents proper middleware execution and may
|
|
103
|
+
create security risks (e.g., PII bypass).
|
|
104
|
+
|
|
105
|
+
See ADR-015 §3.4 for middleware zone guidelines.
|
|
106
|
+
ERROR
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
# Pipeline management for event processing middleware.
|
|
5
|
+
#
|
|
6
|
+
# Provides zone-based pipeline configuration with boot-time validation.
|
|
7
|
+
#
|
|
8
|
+
# @see E11y::Pipeline::Builder
|
|
9
|
+
# @see ADR-015 Middleware Execution Order
|
|
10
|
+
module Pipeline
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Presets
|
|
5
|
+
# Preset for audit events (compliance-critical)
|
|
6
|
+
#
|
|
7
|
+
# Audit events are compliance-critical events that must never be lost,
|
|
8
|
+
# regardless of their severity level. They use a separate audit pipeline that:
|
|
9
|
+
# - Signs events for non-repudiation
|
|
10
|
+
# - Encrypts sensitive data
|
|
11
|
+
# - Routes to audit-specific storage
|
|
12
|
+
# - Skips PII filtering (original data must be preserved)
|
|
13
|
+
#
|
|
14
|
+
# IMPORTANT: Audit events can have ANY severity (info, warn, error, fatal).
|
|
15
|
+
# The severity should be set by the user based on the event's criticality.
|
|
16
|
+
#
|
|
17
|
+
# @example Audit event with info severity (just logging an action)
|
|
18
|
+
# class UserViewedDocumentAudit < E11y::Event::Base
|
|
19
|
+
# include E11y::Presets::AuditEvent
|
|
20
|
+
# severity :info # User explicitly sets severity
|
|
21
|
+
#
|
|
22
|
+
# schema do
|
|
23
|
+
# required(:user_id).filled(:integer)
|
|
24
|
+
# required(:document_id).filled(:integer)
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Audit event with fatal severity (security breach)
|
|
29
|
+
# class SecurityBreachAudit < E11y::Event::Base
|
|
30
|
+
# include E11y::Presets::AuditEvent
|
|
31
|
+
# severity :fatal # User explicitly sets severity
|
|
32
|
+
#
|
|
33
|
+
# schema do
|
|
34
|
+
# required(:breach_type).filled(:string)
|
|
35
|
+
# required(:affected_users).filled(:integer)
|
|
36
|
+
# end
|
|
37
|
+
# end
|
|
38
|
+
module AuditEvent
|
|
39
|
+
def self.included(base)
|
|
40
|
+
base.class_eval do
|
|
41
|
+
# Audit events will use audit pipeline (Phase 4)
|
|
42
|
+
# Severity is NOT set by preset - user decides based on event criticality
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Extend class with audit-specific methods
|
|
46
|
+
base.extend(ClassMethods)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Class methods for audit events
|
|
50
|
+
module ClassMethods
|
|
51
|
+
# Override resolve_rate_limit to unlimited for audit events
|
|
52
|
+
# Audit events must NEVER be dropped, regardless of severity
|
|
53
|
+
def resolve_rate_limit
|
|
54
|
+
nil # Unlimited - compliance requirement
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Override resolve_sample_rate to 100% for audit events
|
|
58
|
+
# Audit events must ALL be tracked, regardless of severity
|
|
59
|
+
def resolve_sample_rate
|
|
60
|
+
1.0 # 100% - compliance requirement
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Presets
|
|
5
|
+
# Preset for debug events (development/troubleshooting)
|
|
6
|
+
#
|
|
7
|
+
# Debug events have:
|
|
8
|
+
# - Low priority (debug severity)
|
|
9
|
+
# - Standard rate limit (1000/sec)
|
|
10
|
+
# - Low sampling (1% - reduces noise)
|
|
11
|
+
# - Only logs adapter (no error tracking/alerting)
|
|
12
|
+
#
|
|
13
|
+
# Adapter name:
|
|
14
|
+
# - :logs → centralized logging (implementation: Loki, Elasticsearch, CloudWatch, etc.)
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# class DebugCacheHitEvent < E11y::Event::Base
|
|
18
|
+
# include E11y::Presets::DebugEvent
|
|
19
|
+
#
|
|
20
|
+
# schema do
|
|
21
|
+
# required(:cache_key).filled(:string)
|
|
22
|
+
# required(:hit).filled(:bool)
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
module DebugEvent
|
|
26
|
+
def self.included(base)
|
|
27
|
+
base.class_eval do
|
|
28
|
+
severity :debug
|
|
29
|
+
adapters :logs # Adapter name
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Presets
|
|
5
|
+
# Preset for high-value events (payments, transactions)
|
|
6
|
+
#
|
|
7
|
+
# High-value events require:
|
|
8
|
+
# - High priority (success severity)
|
|
9
|
+
# - Unlimited rate limit (never drop payment events)
|
|
10
|
+
# - 100% sampling (all payment events)
|
|
11
|
+
# - Multiple adapters (logs + errors_tracker for full observability)
|
|
12
|
+
#
|
|
13
|
+
# Adapter names:
|
|
14
|
+
# - :logs → centralized logging (implementation: Loki, Elasticsearch, CloudWatch, etc.)
|
|
15
|
+
# - :errors_tracker → error tracking with alerting (implementation: Sentry, Rollbar, Bugsnag, etc.)
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# class PaymentProcessedEvent < E11y::Event::Base
|
|
19
|
+
# include E11y::Presets::HighValueEvent
|
|
20
|
+
#
|
|
21
|
+
# schema do
|
|
22
|
+
# required(:payment_id).filled(:integer)
|
|
23
|
+
# required(:amount).filled(:float)
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
module HighValueEvent
|
|
27
|
+
def self.included(base)
|
|
28
|
+
base.class_eval do
|
|
29
|
+
severity :success
|
|
30
|
+
adapters :logs, :errors_tracker # Adapter names
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extend class with overridden methods
|
|
34
|
+
base.extend(ClassMethods)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Class methods that override default behavior
|
|
38
|
+
module ClassMethods
|
|
39
|
+
# Override resolve_rate_limit to unlimited for high-value events
|
|
40
|
+
def resolve_rate_limit
|
|
41
|
+
nil # Unlimited - never drop payment events
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Override resolve_sample_rate to 100% for high-value events
|
|
45
|
+
def resolve_sample_rate
|
|
46
|
+
1.0 # 100% - track all payment events
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/e11y/presets.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
# Presets for common event patterns
|
|
5
|
+
#
|
|
6
|
+
# Presets are modules that can be included in event classes to provide
|
|
7
|
+
# pre-configured settings (severity, adapters, sample rate, etc.)
|
|
8
|
+
#
|
|
9
|
+
# @example Using a preset
|
|
10
|
+
# class MyDebugEvent < E11y::Event::Base
|
|
11
|
+
# include E11y::Presets::DebugEvent
|
|
12
|
+
#
|
|
13
|
+
# schema do
|
|
14
|
+
# required(:debug_info).filled(:string)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
module Presets
|
|
18
|
+
end
|
|
19
|
+
end
|