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,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
@@ -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