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