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,894 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-schema"
4
+ require "e11y/slo/event_driven"
5
+
6
+ module E11y
7
+ module Event
8
+ # Base class for all E11y events using zero-allocation pattern
9
+ #
10
+ # Events are tracked using class methods (not instances) to avoid memory allocations.
11
+ # All event data is stored in Hashes, not objects.
12
+ #
13
+ # @abstract Subclass and define schema using {.schema}
14
+ #
15
+ # @example Define custom event
16
+ # class OrderPaidEvent < E11y::Event::Base
17
+ # schema do
18
+ # required(:order_id).filled(:integer)
19
+ # required(:amount).filled(:float)
20
+ # end
21
+ #
22
+ # severity :success
23
+ # adapters :loki
24
+ # end
25
+ #
26
+ # # Track event (zero-allocation)
27
+ # OrderPaidEvent.track(order_id: 123, amount: 99.99)
28
+ #
29
+ # @see ADR-001 §3.1 Zero-Allocation Design
30
+ # @see UC-002 Business Event Tracking
31
+ # rubocop:disable Metrics/ClassLength
32
+ class Base
33
+ extend SLO::EventDriven::DSL
34
+
35
+ # Severity levels (ordered by importance)
36
+ SEVERITIES = %i[debug info success warn error fatal].freeze
37
+
38
+ # Performance optimization: Inline severity defaults (avoid method call overhead)
39
+ # Used by resolve_sample_rate for fast lookup
40
+ SEVERITY_SAMPLE_RATES = {
41
+ error: 1.0,
42
+ fatal: 1.0,
43
+ debug: 0.01,
44
+ info: 0.1,
45
+ success: 0.1,
46
+ warn: 0.1
47
+ }.freeze
48
+
49
+ # Pre-allocated event_data hash structure (reduce GC pressure)
50
+ # Keys are pre-defined to avoid hash resizing during track()
51
+ EVENT_HASH_TEMPLATE = {
52
+ event_name: nil,
53
+ payload: nil,
54
+ severity: nil,
55
+ version: nil,
56
+ adapters: nil,
57
+ timestamp: nil
58
+ }.freeze
59
+
60
+ # Validation modes for performance tuning
61
+ # - :always (default) - Validate all events (safest, ~60μs overhead)
62
+ # - :sampled - Validate 1% of events (balanced, ~6μs avg overhead)
63
+ # - :never - Skip validation (fastest, ~2μs, use with trusted input only)
64
+ VALIDATION_MODES = %i[always sampled never].freeze
65
+
66
+ # Default validation sampling rate (when mode is :sampled)
67
+ # 1% = catch schema bugs while maintaining high performance
68
+ DEFAULT_VALIDATION_SAMPLE_RATE = 0.01
69
+
70
+ class << self
71
+ # Track an event (zero-allocation pattern)
72
+ #
73
+ # This is the main entry point for all events. No object is created - only a Hash.
74
+ # Returns event hash for testing/debugging. In Phase 2, pipeline will be added.
75
+ #
76
+ # Optimizations applied:
77
+ # - Pre-allocated hash template (reduce GC pressure)
78
+ # - Cached severity/adapters (avoid repeated method calls)
79
+ # - Inline timestamp generation
80
+ # - Configurable validation mode (:always, :sampled, :never)
81
+ #
82
+ # @param payload [Hash] Event data matching the schema
83
+ # @return [Hash] Event hash (includes metadata)
84
+ #
85
+ # @example
86
+ # UserSignupEvent.track(user_id: 123, email: "user@example.com")
87
+ # # => { event_name: "UserSignupEvent", payload: {...}, severity: :info, adapters: [:logs], ... }
88
+ #
89
+ # @raise [E11y::ValidationError] if payload doesn't match schema (when validation runs)
90
+ def track(**payload)
91
+ # 1. Validate payload against schema (respects validation_mode)
92
+ validate_payload!(payload) if should_validate?
93
+
94
+ # 2. Build event hash with metadata (use pre-allocated template, reduce GC)
95
+ # Cache frequently accessed values to avoid method call overhead
96
+ event_severity = severity
97
+ event_adapters = adapters
98
+
99
+ # 3. TODO Phase 2: Send to pipeline
100
+ # E11y::Pipeline.process(event_hash)
101
+
102
+ # 4. Return event hash (pre-allocated structure for performance)
103
+ {
104
+ event_name: event_name,
105
+ payload: payload,
106
+ severity: event_severity,
107
+ version: version,
108
+ adapters: event_adapters,
109
+ timestamp: Time.now.utc.iso8601(3) # ISO8601 with milliseconds
110
+ }
111
+ end
112
+
113
+ # Configure validation mode for performance tuning
114
+ #
115
+ # Modes:
116
+ # - :always (default) - Validate all events (safest, ~60μs P99)
117
+ # Use for: User input, external data, critical events
118
+ #
119
+ # - :sampled (1% by default) - Validate randomly (balanced, ~6μs avg)
120
+ # Use for: High-frequency events with trusted input
121
+ # Catches schema bugs in production without full overhead
122
+ #
123
+ # - :never - Skip all validation (fastest, ~2μs P99)
124
+ # Use for: Hot path events with guaranteed schema compliance
125
+ # Example: Metrics, internal events with typed input
126
+ #
127
+ # @param mode [Symbol] Validation mode (:always, :sampled, :never)
128
+ # @param sample_rate [Float] Sample rate for :sampled mode (0.0-1.0, default: 0.01 = 1%)
129
+ # @return [Symbol] Current validation mode
130
+ #
131
+ # @example Always validate (default, safest)
132
+ # class PaymentEvent < E11y::Event::Base
133
+ # validation_mode :always
134
+ # end
135
+ #
136
+ # @example Sampled validation (balanced performance/safety)
137
+ # class MetricEvent < E11y::Event::Base
138
+ # validation_mode :sampled, sample_rate: 0.01 # 1% validation
139
+ # end
140
+ #
141
+ # @example Never validate (maximum performance, use with caution)
142
+ # class HighFrequencyMetric < E11y::Event::Base
143
+ # validation_mode :never
144
+ # end
145
+ def validation_mode(mode = nil, sample_rate: nil)
146
+ if mode
147
+ unless VALIDATION_MODES.include?(mode)
148
+ raise ArgumentError,
149
+ "Invalid validation mode: #{mode}. Must be one of: #{VALIDATION_MODES.join(', ')}"
150
+ end
151
+
152
+ @validation_mode = mode
153
+ @validation_sample_rate = sample_rate if sample_rate
154
+ end
155
+
156
+ @validation_mode || :always # Default: always validate (safest)
157
+ end
158
+
159
+ # Get current validation sample rate
160
+ #
161
+ # @return [Float] Sample rate (0.0-1.0)
162
+ def validation_sample_rate
163
+ @validation_sample_rate || DEFAULT_VALIDATION_SAMPLE_RATE
164
+ end
165
+
166
+ # Skip validation for hot path events (deprecated, use validation_mode :never)
167
+ #
168
+ # @deprecated Use {validation_mode} instead
169
+ # @param value [Boolean] true to skip validation
170
+ # @return [Boolean] Current skip_validation status
171
+ # rubocop:disable Naming/PredicateMethod
172
+ def skip_validation(value = nil)
173
+ warn "[DEPRECATION] skip_validation is deprecated. Use validation_mode :never instead."
174
+ @validation_mode = :never if value
175
+ @validation_mode == :never
176
+ end
177
+ # rubocop:enable Naming/PredicateMethod
178
+
179
+ # Define event schema using dry-schema
180
+ #
181
+ # @param block [Proc] Schema definition block
182
+ # @yield Block for schema definition
183
+ #
184
+ # @example
185
+ # schema do
186
+ # required(:user_id).filled(:integer)
187
+ # required(:email).filled(:string)
188
+ # end
189
+ def schema(&block)
190
+ @schema_block = block
191
+ end
192
+
193
+ # Get or build schema
194
+ #
195
+ # @return [Dry::Schema::Params, nil] Compiled schema
196
+ def compiled_schema
197
+ return nil unless @schema_block
198
+
199
+ @compiled_schema ||= Dry::Schema.Params(&@schema_block)
200
+ end
201
+
202
+ # Set or get event severity
203
+ #
204
+ # @param value [Symbol, nil] Severity level (debug, info, success, warn, error, fatal)
205
+ # @return [Symbol] Current severity
206
+ #
207
+ # @example
208
+ # class FailureEvent < E11y::Event::Base
209
+ # severity :error
210
+ # end
211
+ def severity(value = nil)
212
+ if value
213
+ unless SEVERITIES.include?(value)
214
+ raise ArgumentError, "Invalid severity: #{value}. Must be one of: #{SEVERITIES.join(', ')}"
215
+ end
216
+
217
+ @severity = value
218
+ end
219
+
220
+ # Return explicitly set severity OR inherit from parent (if set) OR resolve by convention
221
+ return @severity if @severity
222
+ return superclass.severity if superclass != E11y::Event::Base && superclass.instance_variable_get(:@severity)
223
+
224
+ resolved_severity
225
+ end
226
+
227
+ # Set or get event version
228
+ #
229
+ # @param value [Integer, nil] Event version
230
+ # @return [Integer] Current version (default: 1)
231
+ #
232
+ # @example
233
+ # class OrderPaidEventV2 < E11y::Event::Base
234
+ # version 2
235
+ # end
236
+ def version(value = nil)
237
+ @version = value if value
238
+ # Return explicitly set version OR inherit from parent (if set) OR default to 1
239
+ return @version if @version
240
+ return superclass.version if superclass != E11y::Event::Base && superclass.instance_variable_get(:@version)
241
+
242
+ 1
243
+ end
244
+
245
+ # Set or get adapters for this event
246
+ #
247
+ # Adapters are referenced by NAME (e.g., :logs, :errors_tracker).
248
+ # The actual implementation is configured separately in E11y.configuration.
249
+ #
250
+ # @param list [Array<Symbol>, nil] Adapter names
251
+ # @return [Array<Symbol>] Current adapter names
252
+ #
253
+ # @example Using adapter names
254
+ # class CriticalEvent < E11y::Event::Base
255
+ # adapters :logs, :errors_tracker
256
+ # end
257
+ #
258
+ # @example Adapter implementation is configured separately
259
+ # E11y.configure do |config|
260
+ # config.adapters[:logs] = E11y::Adapters::Loki.new(...)
261
+ # config.adapters[:errors_tracker] = E11y::Adapters::Sentry.new(...)
262
+ # end
263
+ def adapters(*list)
264
+ @adapters = list.flatten if list.any?
265
+ # Return explicitly set adapters OR inherit from parent (if set) OR resolve from severity
266
+ return @adapters if @adapters
267
+ return superclass.adapters if superclass != E11y::Event::Base && superclass.instance_variable_get(:@adapters)
268
+
269
+ resolved_adapters
270
+ end
271
+
272
+ # Get event name (normalized)
273
+ #
274
+ # @return [String] Event name without version suffix
275
+ #
276
+ # @example
277
+ # OrderPaidEventV2.event_name # => "OrderPaidEvent"
278
+ def event_name
279
+ # Don't cache for anonymous classes (name returns nil)
280
+ return @event_name if @event_name && name
281
+
282
+ class_name = name || "AnonymousEvent"
283
+ @event_name = class_name.sub(/V\d+$/, "")
284
+ end
285
+
286
+ # Set or get explicit sample rate for this event
287
+ #
288
+ # Sample rate determines what percentage of events to process (0.0-1.0).
289
+ # If not explicitly set, falls back to severity-based defaults.
290
+ #
291
+ # @param value [Float, nil] Sample rate (0.0-1.0)
292
+ # @return [Float, nil] Explicitly set sample rate (nil if using severity-based default)
293
+ #
294
+ # @example Explicit sample rate
295
+ # class HighFrequencyEvent < E11y::Event::Base
296
+ # sample_rate 0.01 # 1% sampling
297
+ # end
298
+ #
299
+ # @example Disable sampling (always process)
300
+ # class CriticalEvent < E11y::Event::Base
301
+ # sample_rate 1.0 # 100% sampling
302
+ # end
303
+ # rubocop:disable Metrics/CyclomaticComplexity
304
+ def sample_rate(value = nil)
305
+ if value
306
+ unless value.is_a?(Numeric) && value >= 0.0 && value <= 1.0
307
+ raise ArgumentError, "Sample rate must be between 0.0 and 1.0, got: #{value.inspect}"
308
+ end
309
+
310
+ @sample_rate = value.to_f
311
+ end
312
+
313
+ # Return explicitly set sample_rate OR inherit from parent (if set) OR nil (use resolve_sample_rate)
314
+ return @sample_rate if @sample_rate
315
+ if superclass != E11y::Event::Base && superclass.instance_variable_get(:@sample_rate)
316
+ return superclass.sample_rate
317
+ end
318
+
319
+ nil
320
+ end
321
+ # rubocop:enable Metrics/CyclomaticComplexity
322
+
323
+ # Configure value-based sampling (FEAT-4849)
324
+ #
325
+ # Prioritize high-value events for sampling based on payload values.
326
+ # Events matching any configured rule will be sampled at 100%.
327
+ #
328
+ # @param field [String, Symbol] Field to extract value from
329
+ # @param comparisons [Hash] Comparison rules
330
+ # @option comparisons [Numeric] :greater_than (>) Sample if value > threshold
331
+ # @option comparisons [Numeric] :less_than (<) Sample if value < threshold
332
+ # @option comparisons [Object] :equals (==) Sample if value == threshold
333
+ # @option comparisons [Range] :in_range Sample if value in range
334
+ # @return [void]
335
+ #
336
+ # @example High-value payments
337
+ # class PaymentEvent < E11y::Event::Base
338
+ # sample_by_value :amount, greater_than: 1000
339
+ # end
340
+ #
341
+ # @example Range-based sampling
342
+ # class OrderEvent < E11y::Event::Base
343
+ # sample_by_value :total, in_range: 100..500
344
+ # end
345
+ def sample_by_value(field, comparisons)
346
+ require "e11y/event/value_sampling_config"
347
+ @value_sampling_configs ||= []
348
+ @value_sampling_configs << ValueSamplingConfig.new(field, comparisons)
349
+ end
350
+
351
+ # Get value-based sampling configurations
352
+ #
353
+ # @return [Array<ValueSamplingConfig>] Configured sampling rules
354
+ def value_sampling_configs
355
+ @value_sampling_configs || []
356
+ end
357
+
358
+ # Resolve sample rate for this event
359
+ #
360
+ # Sample rate determines what percentage of events to process (0.0-1.0)
361
+ # Precedence: explicit sample_rate > severity-based defaults
362
+ # Convention: error/fatal = 1.0 (all), success = 0.1 (10%), debug = 0.01 (1%)
363
+ #
364
+ # Optimized: Uses inline lookup table instead of case statement
365
+ #
366
+ # @return [Float] Sample rate (0.0-1.0)
367
+ def resolve_sample_rate
368
+ # 1. Explicit sample_rate (highest priority)
369
+ return sample_rate if sample_rate
370
+
371
+ # 2. Severity-based defaults (inline lookup, faster than case statement)
372
+ SEVERITY_SAMPLE_RATES[severity] || 0.1
373
+ end
374
+
375
+ # Configure adaptive sampling for this event
376
+ #
377
+ # Adaptive sampling adjusts sample rate dynamically based on conditions.
378
+ # This is a placeholder for future implementation (L2.7 continuation).
379
+ #
380
+ # @param enabled [Boolean] Enable adaptive sampling
381
+ # @param options [Hash] Adaptive sampling options
382
+ # @option options [Float] :error_rate_threshold (0.05) Error rate to trigger 100% sampling
383
+ # @option options [Integer] :load_threshold (50000) Events/sec to trigger reduced sampling
384
+ # @option options [Float] :high_load_sample_rate (0.01) Sample rate during high load
385
+ # @return [Hash, nil] Adaptive sampling configuration
386
+ #
387
+ # @example Enable adaptive sampling
388
+ # class OrderEvent < E11y::Event::Base
389
+ # adaptive_sampling enabled: true,
390
+ # error_rate_threshold: 0.05,
391
+ # load_threshold: 50_000
392
+ # end
393
+ def adaptive_sampling(enabled: false, **options)
394
+ @adaptive_sampling = { enabled: true }.merge(options) if enabled
395
+
396
+ # Return explicitly set config OR inherit from parent (if set) OR nil
397
+ return @adaptive_sampling if @adaptive_sampling
398
+ if superclass != E11y::Event::Base && superclass.instance_variable_get(:@adaptive_sampling)
399
+ return superclass.adaptive_sampling
400
+ end
401
+
402
+ nil
403
+ end
404
+
405
+ # Resolve rate limit for this event (events per second)
406
+ #
407
+ # Rate limit prevents flooding with too many events
408
+ # Convention: error = unlimited, others = 1000/sec
409
+ #
410
+ # @return [Integer, nil] Max events per second (nil = unlimited)
411
+ def resolve_rate_limit
412
+ case severity
413
+ when :error, :fatal
414
+ nil # Unlimited - не теряем ошибки
415
+ else
416
+ 1000 # 1000 events/sec
417
+ end
418
+ end
419
+
420
+ private
421
+
422
+ # Determine if validation should run for this event
423
+ #
424
+ # Respects validation_mode setting:
425
+ # - :always → true (always validate)
426
+ # - :never → false (never validate)
427
+ # - :sampled → random sampling based on validation_sample_rate
428
+ #
429
+ # @return [Boolean] true if validation should run
430
+ def should_validate?
431
+ case validation_mode
432
+ when :never
433
+ false
434
+ when :sampled
435
+ # Random sampling (thread-safe, uses Kernel.rand)
436
+ rand < validation_sample_rate
437
+ else
438
+ # :always or unknown mode - fallback to safe default
439
+ true
440
+ end
441
+ end
442
+
443
+ # Validate payload against schema
444
+ #
445
+ # @param payload [Hash] Event data
446
+ # @raise [E11y::ValidationError] if validation fails
447
+ # @return [void]
448
+ def validate_payload!(payload)
449
+ schema = compiled_schema
450
+ return unless schema # No schema = no validation
451
+
452
+ result = schema.call(payload)
453
+ return if result.success?
454
+
455
+ # Build error message from dry-schema errors
456
+ errors = result.errors.to_h
457
+ raise E11y::ValidationError, "Validation failed for #{event_name}: #{errors.inspect}"
458
+ end
459
+
460
+ # Resolve severity using conventions (CONTRADICTION_01 Solution)
461
+ #
462
+ # Convention: Event name patterns determine severity
463
+ # - *Failed*, *Error* → :error
464
+ # - *Paid*, *Success*, *Completed* → :success
465
+ # - *Warn*, *Warning* → :warn
466
+ # - Default → :info
467
+ #
468
+ # @return [Symbol] Resolved severity
469
+ def resolved_severity
470
+ event_name_str = event_name.to_s
471
+ case event_name_str
472
+ when /Failed/, /Error/
473
+ :error
474
+ when /Paid/, /Success/, /Completed/
475
+ :success
476
+ when /Warn/, /Warning/
477
+ :warn
478
+ else
479
+ :info
480
+ end
481
+ end
482
+
483
+ # Resolve adapters using conventions (CONTRADICTION_01 Solution)
484
+ #
485
+ # Convention: Severity determines adapter names via E11y.configuration
486
+ # Adapter names represent PURPOSE, not implementation.
487
+ #
488
+ # @return [Array<Symbol>] Resolved adapter names
489
+ # @see E11y::Configuration#adapters_for_severity
490
+ def resolved_adapters
491
+ E11y.configuration.adapters_for_severity(severity)
492
+ end
493
+
494
+ public # Make PII and Audit DSL methods public
495
+
496
+ # === PII Filtering DSL (ADR-006, UC-007) ===
497
+
498
+ # Declare whether event contains PII
499
+ #
500
+ # @param value [Boolean] true if event contains PII, false otherwise
501
+ #
502
+ # @example No PII (Tier 1 - Skip filtering)
503
+ # class Events::HealthCheck < E11y::Event::Base
504
+ # contains_pii false
505
+ # end
506
+ #
507
+ # @example Contains PII (Tier 3 - Deep filtering)
508
+ # class Events::UserRegistered < E11y::Event::Base
509
+ # contains_pii true
510
+ #
511
+ # pii_filtering do
512
+ # masks :password
513
+ # hashes :email
514
+ # allows :user_id
515
+ # end
516
+ # end
517
+ def contains_pii(value = nil)
518
+ if value.nil?
519
+ # Getter
520
+ @contains_pii
521
+ else
522
+ # Setter
523
+ @contains_pii = value
524
+ end
525
+ end
526
+
527
+ # Determine the PII filtering tier for this event.
528
+ # @return [Symbol] :tier1, :tier2, or :tier3
529
+ def pii_tier
530
+ case contains_pii
531
+ when false then :tier1
532
+ when true then :tier3
533
+ else :tier2 # Default if not explicitly declared
534
+ end
535
+ end
536
+
537
+ # Define PII filtering rules (DSL block)
538
+ #
539
+ # @yield Block for defining field strategies
540
+ #
541
+ # @example
542
+ # pii_filtering do
543
+ # masks :password, :token
544
+ # hashes :email, :phone
545
+ # allows :user_id, :amount
546
+ # end
547
+ def pii_filtering(&)
548
+ @pii_filtering_config ||= { fields: {} }
549
+ builder = PIIFilteringBuilder.new(@pii_filtering_config)
550
+ builder.instance_eval(&)
551
+ end
552
+
553
+ # Get PII filtering configuration
554
+ #
555
+ # @return [Hash] PII filtering config
556
+ attr_reader :pii_filtering_config
557
+
558
+ # PII Filtering DSL Builder
559
+ #
560
+ # Internal helper class for building PII filtering configuration.
561
+ # Used by {pii_filtering} DSL method.
562
+ #
563
+ # @private
564
+ # @api private
565
+ class PIIFilteringBuilder
566
+ def initialize(config)
567
+ @config = config
568
+ end
569
+
570
+ # Mask fields (replace with [FILTERED])
571
+ #
572
+ # @param fields [Array<Symbol>] Field names to mask
573
+ def masks(*fields)
574
+ fields.each { |field| @config[:fields][field] = { strategy: :mask } }
575
+ end
576
+
577
+ # Hash fields (one-way hash with SHA256)
578
+ #
579
+ # @param fields [Array<Symbol>] Field names to hash
580
+ def hashes(*fields)
581
+ fields.each { |field| @config[:fields][field] = { strategy: :hash } }
582
+ end
583
+
584
+ # Partial mask fields (show first/last chars)
585
+ #
586
+ # @param fields [Array<Symbol>] Field names to partially mask
587
+ def partials(*fields)
588
+ fields.each { |field| @config[:fields][field] = { strategy: :partial } }
589
+ end
590
+
591
+ # Redact fields (remove completely)
592
+ #
593
+ # @param fields [Array<Symbol>] Field names to redact
594
+ def redacts(*fields)
595
+ fields.each { |field| @config[:fields][field] = { strategy: :redact } }
596
+ end
597
+
598
+ # Allow fields (no filtering)
599
+ #
600
+ # @param fields [Array<Symbol>] Field names to allow
601
+ def allows(*fields)
602
+ fields.each { |field| @config[:fields][field] = { strategy: :allow } }
603
+ end
604
+ end
605
+
606
+ # === Audit Event DSL (ADR-006, UC-012) ===
607
+
608
+ # Mark event as audit event
609
+ #
610
+ # Audit events use separate pipeline:
611
+ # - Sign ORIGINAL data (before PII filtering)
612
+ # - Never sampled or rate-limited
613
+ # - Stored in encrypted audit storage
614
+ #
615
+ # @param value [Boolean] true if audit event
616
+ #
617
+ # @example
618
+ # class Events::UserDeleted < E11y::Event::Base
619
+ # audit_event true
620
+ #
621
+ # schema do
622
+ # required(:user_id).filled(:integer)
623
+ # required(:deleted_by).filled(:integer)
624
+ # end
625
+ # end
626
+ def audit_event(value = nil)
627
+ if value.nil?
628
+ # Getter
629
+ @audit_event
630
+ else
631
+ # Setter
632
+ @audit_event = value
633
+ end
634
+ end
635
+
636
+ # Check if event is audit event
637
+ #
638
+ # @return [Boolean] true if audit event
639
+ def audit_event?
640
+ @audit_event == true
641
+ end
642
+
643
+ # Configure cryptographic signing for audit event
644
+ #
645
+ # By default, all audit events are signed with HMAC-SHA256.
646
+ # Use `signing enabled: false` to disable signing for specific events.
647
+ #
648
+ # **DESIGN CONSISTENCY**: Matches `E11y.configure { config.audit_trail { signing enabled: true } }`
649
+ #
650
+ # @param options [Hash] Signing configuration
651
+ # @option options [Boolean] :enabled (true) Enable/disable signing for this event
652
+ #
653
+ # @example Disable signing for low-severity audit event
654
+ # class Events::AuditLogViewed < E11y::Event::Base
655
+ # audit_event true
656
+ # signing enabled: false # ← No cryptographic signing
657
+ #
658
+ # schema do
659
+ # required(:log_id).filled(:integer)
660
+ # required(:viewed_by).filled(:integer)
661
+ # end
662
+ # end
663
+ #
664
+ # @example Signing enabled (default)
665
+ # class Events::UserDeleted < E11y::Event::Base
666
+ # audit_event true
667
+ # # signing enabled: true (default) - signing enabled
668
+ #
669
+ # schema do
670
+ # required(:user_id).filled(:integer)
671
+ # end
672
+ # end
673
+ def signing(options = nil)
674
+ if options.nil?
675
+ # Getter: return current config
676
+ @signing_config ||= { enabled: true }
677
+ else
678
+ # Setter: merge with defaults
679
+ @signing_config = { enabled: true }.merge(options)
680
+ end
681
+ end
682
+
683
+ # Check if signing is enabled for this event
684
+ #
685
+ # @return [Boolean] true if signing enabled (default: true)
686
+ def signing_enabled?
687
+ signing[:enabled] != false
688
+ end
689
+
690
+ # Check if event requires signing
691
+ #
692
+ # @return [Boolean] true if event requires signing
693
+ def requires_signing?
694
+ audit_event? && signing_enabled?
695
+ end
696
+
697
+ # === Metrics DSL (ADR-002, UC-003) ===
698
+
699
+ # Define metrics for this event
700
+ #
701
+ # Metrics are automatically registered in E11y::Metrics::Registry
702
+ # and validated for label conflicts at boot time.
703
+ #
704
+ # @yield Block for defining metrics
705
+ #
706
+ # @example Counter metric
707
+ # class Events::OrderCreated < E11y::Event::Base
708
+ # metrics do
709
+ # counter :orders_total, tags: [:currency, :status]
710
+ # end
711
+ # end
712
+ #
713
+ # @example Histogram metric
714
+ # class Events::OrderPaid < E11y::Event::Base
715
+ # metrics do
716
+ # histogram :order_amount,
717
+ # value: :amount,
718
+ # tags: [:currency],
719
+ # buckets: [10, 50, 100, 500, 1000]
720
+ # end
721
+ # end
722
+ #
723
+ # @example Gauge metric
724
+ # class Events::QueueSize < E11y::Event::Base
725
+ # metrics do
726
+ # gauge :queue_depth, value: :size, tags: [:queue_name]
727
+ # end
728
+ # end
729
+ #
730
+ # @example Multiple metrics
731
+ # class Events::OrderPaid < E11y::Event::Base
732
+ # metrics do
733
+ # counter :orders_total, tags: [:currency, :status]
734
+ # histogram :order_amount, value: :amount, tags: [:currency]
735
+ # end
736
+ # end
737
+ def metrics(&block)
738
+ return @metrics_config unless block
739
+
740
+ @metrics_config ||= []
741
+ builder = MetricsBuilder.new(@metrics_config, event_name)
742
+ builder.instance_eval(&block)
743
+
744
+ # Register metrics in global registry
745
+ register_metrics_in_registry!
746
+ end
747
+
748
+ # Get metrics configuration
749
+ #
750
+ # @return [Array<Hash>] Metrics configuration
751
+ def metrics_config
752
+ @metrics_config || []
753
+ end
754
+
755
+ private
756
+
757
+ # Register metrics in global registry
758
+ #
759
+ # This is called after metrics DSL block is evaluated.
760
+ # Validates for label conflicts at boot time.
761
+ def register_metrics_in_registry!
762
+ return if @metrics_config.nil? || @metrics_config.empty?
763
+
764
+ registry = E11y::Metrics::Registry.instance
765
+ @metrics_config.each do |metric_config|
766
+ registry.register(metric_config.merge(
767
+ pattern: event_name, # Exact match for event-level metrics
768
+ source: "#{name}.metrics"
769
+ ))
770
+ end
771
+ end
772
+
773
+ # Metrics DSL Builder
774
+ #
775
+ # Internal helper class for building metrics configuration.
776
+ # Used by {metrics} DSL method.
777
+ #
778
+ # @private
779
+ # @api private
780
+ class MetricsBuilder
781
+ def initialize(config, event_name)
782
+ @config = config
783
+ @event_name = event_name
784
+ end
785
+
786
+ # Define a counter metric
787
+ #
788
+ # Counter metrics track the number of times an event occurs.
789
+ #
790
+ # @param name [Symbol] Metric name (e.g., :orders_total)
791
+ # @param tags [Array<Symbol>] Labels to extract from event data
792
+ #
793
+ # @example
794
+ # counter :orders_total, tags: [:currency, :status]
795
+ def counter(name, tags: [])
796
+ @config << {
797
+ type: :counter,
798
+ name: name,
799
+ tags: tags
800
+ }
801
+ end
802
+
803
+ # Define a histogram metric
804
+ #
805
+ # Histogram metrics track the distribution of values.
806
+ #
807
+ # @param name [Symbol] Metric name (e.g., :order_amount)
808
+ # @param value [Symbol, Proc] Value extractor (field name or lambda)
809
+ # @param tags [Array<Symbol>] Labels to extract from event data
810
+ # @param buckets [Array<Numeric>] Histogram buckets (optional)
811
+ #
812
+ # @example With field name
813
+ # histogram :order_amount, value: :amount, tags: [:currency]
814
+ #
815
+ # @example With lambda
816
+ # histogram :order_amount,
817
+ # value: ->(event) { event[:payload][:amount] },
818
+ # tags: [:currency]
819
+ def histogram(name, value:, tags: [], buckets: nil)
820
+ @config << {
821
+ type: :histogram,
822
+ name: name,
823
+ value: value,
824
+ tags: tags,
825
+ buckets: buckets
826
+ }.compact
827
+ end
828
+
829
+ # Define a gauge metric
830
+ #
831
+ # Gauge metrics track the current value of something.
832
+ #
833
+ # @param name [Symbol] Metric name (e.g., :queue_depth)
834
+ # @param value [Symbol, Proc] Value extractor (field name or lambda)
835
+ # @param tags [Array<Symbol>] Labels to extract from event data
836
+ #
837
+ # @example
838
+ # gauge :queue_depth, value: :size, tags: [:queue_name]
839
+ def gauge(name, value:, tags: [])
840
+ @config << {
841
+ type: :gauge,
842
+ name: name,
843
+ value: value,
844
+ tags: tags
845
+ }
846
+ end
847
+ end
848
+ end
849
+
850
+ # Builder for PII filtering DSL
851
+ class PIIFilteringBuilder
852
+ def initialize(config)
853
+ @config = config
854
+ end
855
+
856
+ # Mask fields (strategy: :mask)
857
+ def masks(*fields)
858
+ fields.each do |field|
859
+ @config[:fields][field] = { strategy: :mask }
860
+ end
861
+ end
862
+
863
+ # Hash fields (strategy: :hash)
864
+ def hashes(*fields)
865
+ fields.each do |field|
866
+ @config[:fields][field] = { strategy: :hash }
867
+ end
868
+ end
869
+
870
+ # Allow fields (strategy: :allow)
871
+ def allows(*fields)
872
+ fields.each do |field|
873
+ @config[:fields][field] = { strategy: :allow }
874
+ end
875
+ end
876
+
877
+ # Partial mask fields (strategy: :partial)
878
+ def partials(*fields)
879
+ fields.each do |field|
880
+ @config[:fields][field] = { strategy: :partial }
881
+ end
882
+ end
883
+
884
+ # Redact fields (strategy: :redact)
885
+ def redacts(*fields)
886
+ fields.each do |field|
887
+ @config[:fields][field] = { strategy: :redact }
888
+ end
889
+ end
890
+ end
891
+ end
892
+ # rubocop:enable Metrics/ClassLength
893
+ end
894
+ end