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,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module E11y
6
+ module Metrics
7
+ # Registry for metric configurations.
8
+ #
9
+ # Stores metric definitions and provides pattern-based matching.
10
+ # This is a singleton class - use Registry.instance to access it.
11
+ # All metrics (global, event-level, preset) are registered here for validation.
12
+ #
13
+ # @example Register metrics
14
+ # registry = E11y::Metrics::Registry.instance
15
+ # registry.register(
16
+ # type: :counter,
17
+ # pattern: 'order.*',
18
+ # name: :orders_total,
19
+ # tags: [:status, :currency],
20
+ # source: 'config/initializers/e11y.rb'
21
+ # )
22
+ #
23
+ # @example Find matching metrics
24
+ # metrics = registry.find_matching('order.paid')
25
+ # # => [{ type: :counter, name: :orders_total, ... }]
26
+ class Registry
27
+ include Singleton
28
+
29
+ # Custom error for label conflicts
30
+ class LabelConflictError < StandardError; end
31
+
32
+ # Custom error for type conflicts
33
+ class TypeConflictError < StandardError; end
34
+
35
+ def initialize
36
+ @metrics = []
37
+ @mutex = Mutex.new
38
+ end
39
+
40
+ # Register a new metric configuration
41
+ #
42
+ # Validates for conflicts with existing metrics:
43
+ # - Same metric name must have same type
44
+ # - Same metric name must have same labels (tags)
45
+ # - Same metric name must have same buckets (for histograms)
46
+ #
47
+ # @param config [Hash] Metric configuration
48
+ # @option config [Symbol] :type Metric type (:counter, :histogram, :gauge)
49
+ # @option config [String] :pattern Glob pattern for event names
50
+ # @option config [Symbol] :name Metric name
51
+ # @option config [Array<Symbol>] :tags Label names to extract
52
+ # @option config [Proc, Symbol] :value Value extractor (for histogram/gauge)
53
+ # @option config [Array<Numeric>] :buckets Histogram buckets
54
+ # @option config [String] :source Source of the metric (for error messages)
55
+ # @return [void]
56
+ #
57
+ # @raise [LabelConflictError] if metric already registered with different labels
58
+ # @raise [TypeConflictError] if metric already registered with different type
59
+ def register(config)
60
+ validate_config!(config)
61
+
62
+ @mutex.synchronize do
63
+ # Check for conflicts with existing metrics (find within lock)
64
+ existing = @metrics.find { |m| m[:name] == config[:name] }
65
+ validate_no_conflicts!(existing, config) if existing
66
+
67
+ @metrics << config.merge(
68
+ pattern_regex: compile_pattern(config[:pattern])
69
+ )
70
+ end
71
+ end
72
+
73
+ # Find all metrics matching the event name
74
+ # @param event_name [String] Event name to match
75
+ # @return [Array<Hash>] Matching metric configurations
76
+ def find_matching(event_name)
77
+ @mutex.synchronize do
78
+ @metrics.select do |metric|
79
+ metric[:pattern_regex].match?(event_name)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Find metric by name (for conflict detection)
85
+ # @param name [Symbol] Metric name
86
+ # @return [Hash, nil] Metric configuration or nil
87
+ def find_by_name(name)
88
+ @mutex.synchronize do
89
+ @metrics.find { |m| m[:name] == name }
90
+ end
91
+ end
92
+
93
+ # Get all registered metrics
94
+ # @return [Array<Hash>] All metric configurations
95
+ def all
96
+ @mutex.synchronize { @metrics.dup }
97
+ end
98
+
99
+ # Clear all registered metrics
100
+ # @return [void]
101
+ def clear!
102
+ @mutex.synchronize { @metrics.clear }
103
+ end
104
+
105
+ # Get count of registered metrics
106
+ # @return [Integer] Number of registered metrics
107
+ def size
108
+ @mutex.synchronize { @metrics.size }
109
+ end
110
+
111
+ # Validate all registered metrics for conflicts
112
+ #
113
+ # This method is called at Rails boot time to catch configuration errors early.
114
+ # It re-validates all metrics to ensure no conflicts exist.
115
+ #
116
+ # @raise [LabelConflictError] if metrics have conflicting labels
117
+ # @raise [TypeConflictError] if metrics have conflicting types
118
+ # @return [void]
119
+ #
120
+ # @example Manual validation
121
+ # E11y::Metrics::Registry.instance.validate_all!
122
+ def validate_all!
123
+ @mutex.synchronize do
124
+ # Group metrics by name
125
+ metrics_by_name = @metrics.group_by { |m| m[:name] }
126
+
127
+ # Check each group for conflicts
128
+ metrics_by_name.each_value do |metrics|
129
+ next if metrics.size == 1 # No conflicts possible
130
+
131
+ # Compare first metric with all others
132
+ first = metrics.first
133
+ metrics[1..].each do |metric|
134
+ validate_no_conflicts!(first, metric)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ # Validate metric configuration
143
+ # @param config [Hash] Metric configuration
144
+ # @raise [ArgumentError] if configuration is invalid
145
+ # rubocop:disable Metrics/AbcSize
146
+ def validate_config!(config)
147
+ raise ArgumentError, "Metric type is required" unless config[:type]
148
+ raise ArgumentError, "Invalid metric type: #{config[:type]}" unless %i[counter histogram
149
+ gauge].include?(config[:type])
150
+ raise ArgumentError, "Pattern is required" unless config[:pattern]
151
+ raise ArgumentError, "Metric name is required" unless config[:name]
152
+
153
+ return unless %i[histogram gauge].include?(config[:type]) && !config[:value]
154
+
155
+ raise ArgumentError, "Value extractor is required for #{config[:type]} metrics"
156
+ end
157
+ # rubocop:enable Metrics/AbcSize
158
+
159
+ # Validate that new metric doesn't conflict with existing one
160
+ # @param existing [Hash] Existing metric configuration
161
+ # @param new_config [Hash] New metric configuration
162
+ # @raise [TypeConflictError] if types don't match
163
+ # @raise [LabelConflictError] if labels don't match
164
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
165
+ def validate_no_conflicts!(existing, new_config)
166
+ # Check 1: Type must match
167
+ if existing[:type] != new_config[:type]
168
+ raise TypeConflictError, <<~ERROR
169
+ Metric "#{new_config[:name]}" type conflict!
170
+
171
+ Existing: #{existing[:type]} (from #{existing[:source] || 'unknown'})
172
+ New: #{new_config[:type]} (from #{new_config[:source] || 'unknown'})
173
+
174
+ Fix: Use the same type everywhere or rename the metric.
175
+ ERROR
176
+ end
177
+
178
+ # Check 2: Labels (tags) must match
179
+ existing_tags = (existing[:tags] || []).sort
180
+ new_tags = (new_config[:tags] || []).sort
181
+
182
+ if existing_tags != new_tags
183
+ raise LabelConflictError, <<~ERROR
184
+ Metric "#{new_config[:name]}" label conflict!
185
+
186
+ Existing: #{existing_tags.inspect} (from #{existing[:source] || 'unknown'})
187
+ New: #{new_tags.inspect} (from #{new_config[:source] || 'unknown'})
188
+
189
+ Fix: Use the same labels everywhere or rename the metric.
190
+
191
+ Example:
192
+ # Event 1
193
+ counter :#{new_config[:name]}, tags: #{existing_tags.inspect}
194
+ #{' '}
195
+ # Event 2 (must match!)
196
+ counter :#{new_config[:name]}, tags: #{existing_tags.inspect}
197
+ ERROR
198
+ end
199
+
200
+ # Check 3: For histograms, buckets should match (warn only)
201
+ return unless new_config[:type] == :histogram
202
+
203
+ existing_buckets = existing[:buckets]
204
+ new_buckets = new_config[:buckets]
205
+
206
+ return if existing_buckets == new_buckets
207
+
208
+ warn <<~WARNING
209
+ Metric "#{new_config[:name]}" has different buckets!
210
+ Existing: #{existing_buckets.inspect}
211
+ New: #{new_buckets.inspect}
212
+ Using existing buckets.
213
+ WARNING
214
+ end
215
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
216
+
217
+ # Compile glob pattern to regex
218
+ # @param pattern [String] Glob pattern (e.g., "order.*", "user.*.created")
219
+ # @return [Regexp] Compiled regex
220
+ def compile_pattern(pattern)
221
+ # Convert glob pattern to regex
222
+ # - '*' matches any segment (e.g., "order.*" matches "order.paid", "order.created")
223
+ # - '**' matches any number of segments (e.g., "order.**" matches "order.paid.usd")
224
+ regex_pattern = pattern
225
+ .gsub(".", '\.') # Escape dots
226
+ .gsub("**", "__DOUBLE__") # Temporarily replace **
227
+ .gsub("*", "[^.]+") # * matches single segment
228
+ .gsub("__DOUBLE__", ".*") # ** matches multiple segments
229
+
230
+ Regexp.new("\\A#{regex_pattern}\\z")
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Metrics
5
+ # Universal label relabeling mechanism via DSL.
6
+ #
7
+ # Transforms high-cardinality label values into low-cardinality categories
8
+ # to prevent metric explosions while preserving signal quality.
9
+ #
10
+ # @example HTTP status codes to classes
11
+ # relabeler = Relabeling.new
12
+ # relabeler.define(:http_status) do |value|
13
+ # case value.to_i
14
+ # when 100..199 then '1xx'
15
+ # when 200..299 then '2xx'
16
+ # when 300..399 then '3xx'
17
+ # when 400..499 then '4xx'
18
+ # when 500..599 then '5xx'
19
+ # else 'unknown'
20
+ # end
21
+ # end
22
+ #
23
+ # relabeler.apply(:http_status, 200) # => '2xx'
24
+ # relabeler.apply(:http_status, 404) # => '4xx'
25
+ #
26
+ # @example Path normalization
27
+ # relabeler.define(:path) do |value|
28
+ # value.gsub(/\/\d+/, '/:id')
29
+ # .gsub(/\/[a-f0-9-]{36}/, '/:uuid')
30
+ # end
31
+ #
32
+ # relabeler.apply(:path, '/users/123/orders/456') # => '/users/:id/orders/:id'
33
+ #
34
+ # @example Region grouping
35
+ # relabeler.define(:region) do |value|
36
+ # case value.to_s
37
+ # when /^us-/ then 'us'
38
+ # when /^eu-/ then 'eu'
39
+ # when /^ap-/ then 'ap'
40
+ # else 'other'
41
+ # end
42
+ # end
43
+ #
44
+ # @see ADR-002 §4.6 (Relabeling Rules)
45
+ class Relabeling
46
+ # Initialize relabeler with optional rules
47
+ #
48
+ # @param rules [Hash{Symbol => Proc}] Initial relabeling rules
49
+ def initialize(rules = {})
50
+ @rules = {}
51
+ @mutex = Mutex.new
52
+ rules.each { |label_key, block| define(label_key, &block) }
53
+ end
54
+
55
+ # Define relabeling rule for a label
56
+ #
57
+ # Rule is applied via block that receives original value
58
+ # and returns transformed value.
59
+ #
60
+ # @param label_key [Symbol, String] Label key to relabel
61
+ # @yield [value] Block that transforms label value
62
+ # @yieldparam value [Object] Original label value
63
+ # @yieldreturn [String, Symbol] Transformed label value
64
+ # @return [void]
65
+ #
66
+ # @example Simple mapping
67
+ # relabeler.define(:environment) do |value|
68
+ # value == 'production' ? 'prod' : 'non-prod'
69
+ # end
70
+ #
71
+ # @example Range-based classification
72
+ # relabeler.define(:duration_ms) do |value|
73
+ # case value.to_i
74
+ # when 0..100 then 'fast'
75
+ # when 101..1000 then 'medium'
76
+ # else 'slow'
77
+ # end
78
+ # end
79
+ def define(label_key, &block)
80
+ raise ArgumentError, "Block required for relabeling rule" unless block_given?
81
+
82
+ @mutex.synchronize do
83
+ @rules[label_key.to_sym] = block
84
+ end
85
+ end
86
+
87
+ # Apply relabeling to label value
88
+ #
89
+ # If rule exists for label_key, applies transformation.
90
+ # Otherwise returns original value unchanged.
91
+ #
92
+ # Thread-safe operation.
93
+ #
94
+ # @param label_key [Symbol, String] Label key
95
+ # @param value [Object] Original value
96
+ # @return [Object] Relabeled value or original if no rule defined
97
+ def apply(label_key, value)
98
+ rule = @mutex.synchronize { @rules[label_key.to_sym] }
99
+ return value unless rule
100
+
101
+ begin
102
+ rule.call(value)
103
+ rescue StandardError => e
104
+ warn "[E11y] Relabeling error for #{label_key}=#{value}: #{e.message}"
105
+ value # Return original on error
106
+ end
107
+ end
108
+
109
+ # Apply relabeling to hash of labels
110
+ #
111
+ # Transforms all labels that have defined rules.
112
+ # Labels without rules pass through unchanged.
113
+ #
114
+ # @param labels [Hash] Hash of label_key => value
115
+ # @return [Hash] Hash with relabeled values
116
+ #
117
+ # @example
118
+ # labels = { http_status: 200, path: '/users/123', env: 'production' }
119
+ # relabeler.apply_all(labels)
120
+ # # => { http_status: '2xx', path: '/users/:id', env: 'production' }
121
+ def apply_all(labels)
122
+ labels.transform_keys(&:to_sym).transform_values do |value|
123
+ label_key = begin
124
+ labels.key(value).to_sym
125
+ rescue StandardError
126
+ nil
127
+ end
128
+ label_key ? apply(label_key, value) : value
129
+ end
130
+ end
131
+
132
+ # Check if relabeling rule exists for label
133
+ #
134
+ # @param label_key [Symbol, String] Label key
135
+ # @return [Boolean] true if rule defined
136
+ def defined?(label_key)
137
+ @mutex.synchronize { @rules.key?(label_key.to_sym) }
138
+ end
139
+
140
+ # Remove relabeling rule
141
+ #
142
+ # @param label_key [Symbol, String] Label key
143
+ # @return [void]
144
+ def remove(label_key)
145
+ @mutex.synchronize { @rules.delete(label_key.to_sym) }
146
+ end
147
+
148
+ # Get all defined rule keys
149
+ #
150
+ # @return [Array<Symbol>] List of label keys with rules
151
+ def keys
152
+ @mutex.synchronize { @rules.keys }
153
+ end
154
+
155
+ # Reset all relabeling rules
156
+ #
157
+ # @return [void]
158
+ def reset!
159
+ @mutex.synchronize { @rules.clear }
160
+ end
161
+
162
+ # Get number of defined rules
163
+ #
164
+ # @return [Integer] Rule count
165
+ def size
166
+ @mutex.synchronize { @rules.size }
167
+ end
168
+
169
+ # Predefined common relabeling rules
170
+ module CommonRules
171
+ # HTTP status code to status class (1xx, 2xx, 3xx, 4xx, 5xx)
172
+ #
173
+ # @param value [Integer, String] HTTP status code
174
+ # @return [String] Status class
175
+ def self.http_status_class(value)
176
+ code = value.to_i
177
+ return "unknown" if code < 100 || code >= 600
178
+
179
+ "#{code / 100}xx"
180
+ end
181
+
182
+ # Path normalization - replace numeric IDs and UUIDs with placeholders
183
+ #
184
+ # @param value [String] URL path
185
+ # @return [String] Normalized path
186
+ def self.normalize_path(value)
187
+ value.to_s
188
+ .gsub(%r{/[a-f0-9-]{36}}, "/:uuid") # UUIDs (must be before :id to avoid partial match)
189
+ .gsub(%r{/[a-f0-9]{32}}, "/:hash") # MD5 hashes (must be before :id)
190
+ .gsub(%r{/\d+}, "/:id") # /users/123 -> /users/:id
191
+ end
192
+
193
+ # Region to region group (us-east-1 -> us, eu-west-2 -> eu)
194
+ #
195
+ # @param value [String] AWS-style region
196
+ # @return [String] Region group
197
+ def self.region_group(value)
198
+ case value.to_s
199
+ when /^us-/ then "us"
200
+ when /^eu-/ then "eu"
201
+ when /^ap-/ then "ap"
202
+ when /^sa-/ then "sa"
203
+ when /^ca-/ then "ca"
204
+ when /^af-/ then "af"
205
+ when /^me-/ then "me"
206
+ else "other"
207
+ end
208
+ end
209
+
210
+ # Duration classification (ms to fast/medium/slow)
211
+ #
212
+ # @param value [Numeric] Duration in milliseconds
213
+ # @return [String] Classification
214
+ def self.duration_class(value)
215
+ ms = value.to_f
216
+ case ms
217
+ when 0..100 then "fast"
218
+ when 101..1000 then "medium"
219
+ when 1001..5000 then "slow"
220
+ else "very_slow"
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/metrics/registry"
4
+ require "e11y/metrics/cardinality_protection"
5
+
6
+ module E11y
7
+ # Public API for tracking metrics.
8
+ #
9
+ # This is a facade that delegates to the configured metrics backend (e.g., Yabeda).
10
+ # If no backend is configured, metrics are silently discarded (noop).
11
+ #
12
+ # @example Track a counter
13
+ # E11y::Metrics.increment(:http_requests_total, { method: 'GET', status: 200 })
14
+ #
15
+ # @example Track a histogram
16
+ # E11y::Metrics.histogram(:http_request_duration_seconds, 0.042, { method: 'GET' })
17
+ #
18
+ # @example Track a gauge
19
+ # E11y::Metrics.gauge(:active_connections, 42, { server: 'web-01' })
20
+ #
21
+ # @see ADR-002 §3 (Metrics Integration)
22
+ # @see ADR-016 §3 (Self-Monitoring Metrics)
23
+ module Metrics
24
+ class << self
25
+ # Track a counter metric (monotonically increasing value).
26
+ #
27
+ # @param name [Symbol] Metric name (e.g., :http_requests_total)
28
+ # @param labels [Hash] Metric labels (e.g., { method: 'GET', status: 200 })
29
+ # @param value [Integer] Increment value (default: 1)
30
+ # @return [void]
31
+ #
32
+ # @example
33
+ # E11y::Metrics.increment(:e11y_events_tracked, { event_type: 'order.created' })
34
+ def increment(name, labels = {}, value: 1)
35
+ backend&.increment(name, labels, value: value)
36
+ end
37
+
38
+ # Track a histogram metric (distribution of values).
39
+ #
40
+ # @param name [Symbol] Metric name (e.g., :http_request_duration_seconds)
41
+ # @param value [Numeric] Observed value (e.g., 0.042 for 42ms)
42
+ # @param labels [Hash] Metric labels (e.g., { method: 'GET' })
43
+ # @param buckets [Array<Numeric>, nil] Optional histogram buckets (for backend config)
44
+ # @return [void]
45
+ #
46
+ # @example
47
+ # E11y::Metrics.histogram(:e11y_track_duration_seconds, 0.0005, { event_type: 'order.created' })
48
+ def histogram(name, value, labels = {}, buckets: nil)
49
+ backend&.histogram(name, value, labels, buckets: buckets)
50
+ end
51
+
52
+ # Track a gauge metric (current value that can go up or down).
53
+ #
54
+ # @param name [Symbol] Metric name (e.g., :active_connections)
55
+ # @param value [Numeric] Current value (e.g., 42)
56
+ # @param labels [Hash] Metric labels (e.g., { server: 'web-01' })
57
+ # @return [void]
58
+ #
59
+ # @example
60
+ # E11y::Metrics.gauge(:e11y_buffer_size, 128, { buffer_type: 'ring' })
61
+ def gauge(name, value, labels = {})
62
+ backend&.gauge(name, value, labels)
63
+ end
64
+
65
+ # Get the configured metrics backend.
66
+ #
67
+ # The backend is determined by checking for configured adapters:
68
+ # - If Yabeda adapter is configured, use it
69
+ # - Otherwise, return nil (noop)
70
+ #
71
+ # @return [Object, nil] Metrics backend or nil
72
+ # @api private
73
+ def backend
74
+ return @backend if defined?(@backend)
75
+
76
+ @backend = detect_backend
77
+ end
78
+
79
+ # Reset the backend (useful for testing).
80
+ #
81
+ # @api private
82
+ def reset_backend!
83
+ remove_instance_variable(:@backend) if defined?(@backend)
84
+ end
85
+
86
+ private
87
+
88
+ # Detect the metrics backend from configured adapters.
89
+ #
90
+ # @return [Object, nil] Metrics backend or nil
91
+ def detect_backend
92
+ # Check if Yabeda adapter is configured
93
+ # Use class name string to avoid LoadError if Yabeda gem not installed
94
+ yabeda_adapter = E11y.config.adapters.values.find { |adapter| adapter.class.name == "E11y::Adapters::Yabeda" }
95
+ return yabeda_adapter if yabeda_adapter
96
+
97
+ # No backend configured → noop
98
+ nil
99
+ end
100
+ end
101
+ end
102
+ end