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,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Check if Sentry SDK is available
4
+ begin
5
+ require "sentry-ruby"
6
+ rescue LoadError
7
+ raise LoadError, <<~ERROR
8
+ Sentry SDK not available!
9
+
10
+ To use E11y::Adapters::Sentry, add to your Gemfile:
11
+
12
+ gem 'sentry-ruby'
13
+
14
+ Then run: bundle install
15
+ ERROR
16
+ end
17
+
18
+ module E11y
19
+ module Adapters
20
+ # Sentry adapter for error tracking and breadcrumbs.
21
+ #
22
+ # Features:
23
+ # - Automatic error reporting to Sentry
24
+ # - Breadcrumb tracking for context
25
+ # - Severity-based filtering
26
+ # - Trace context propagation
27
+ # - User context support
28
+ #
29
+ # @example Basic usage
30
+ # adapter = E11y::Adapters::Sentry.new(
31
+ # dsn: ENV["SENTRY_DSN"],
32
+ # environment: "production",
33
+ # severity_threshold: :warn
34
+ # )
35
+ #
36
+ # @example With Registry
37
+ # E11y::Adapters::Registry.register(
38
+ # :error_tracker,
39
+ # E11y::Adapters::Sentry.new(dsn: ENV["SENTRY_DSN"])
40
+ # )
41
+ #
42
+ # @see https://docs.sentry.io/platforms/ruby/
43
+ class Sentry < Base
44
+ # Severity levels in order
45
+ SEVERITY_LEVELS = %i[debug info success warn error fatal].freeze
46
+
47
+ # Default severity threshold for Sentry
48
+ DEFAULT_SEVERITY_THRESHOLD = :warn
49
+
50
+ attr_reader :dsn, :environment, :severity_threshold, :send_breadcrumbs
51
+
52
+ # Initialize Sentry adapter
53
+ #
54
+ # @param config [Hash] Configuration options
55
+ # @option config [String] :dsn (required) Sentry DSN
56
+ # @option config [String] :environment ("production") Environment name
57
+ # @option config [Symbol] :severity_threshold (:warn) Minimum severity to send to Sentry
58
+ # @option config [Boolean] :breadcrumbs (true) Enable breadcrumb tracking
59
+ def initialize(config = {})
60
+ @dsn = config[:dsn]
61
+ @environment = config.fetch(:environment, "production")
62
+ @severity_threshold = config.fetch(:severity_threshold, DEFAULT_SEVERITY_THRESHOLD)
63
+ @send_breadcrumbs = config.fetch(:breadcrumbs, true)
64
+
65
+ super
66
+
67
+ initialize_sentry!
68
+ end
69
+
70
+ # Write event to Sentry
71
+ #
72
+ # @param event_data [Hash] Event payload
73
+ # @return [Boolean] Success status
74
+ def write(event_data)
75
+ severity = event_data[:severity]
76
+
77
+ # Only send events above threshold
78
+ return true unless should_send_to_sentry?(severity)
79
+
80
+ if error_severity?(severity)
81
+ send_error_to_sentry(event_data)
82
+ elsif @send_breadcrumbs
83
+ send_breadcrumb_to_sentry(event_data)
84
+ end
85
+
86
+ true
87
+ rescue StandardError => e
88
+ warn "E11y Sentry adapter error: #{e.message}"
89
+ false
90
+ end
91
+
92
+ # Adapter capabilities
93
+ #
94
+ # @return [Hash] Capability flags
95
+ def capabilities
96
+ super.merge(
97
+ batching: false, # Sentry SDK handles batching
98
+ compression: false, # Sentry SDK handles compression
99
+ async: true, # Sentry SDK is async
100
+ streaming: false
101
+ )
102
+ end
103
+
104
+ # Check if adapter is healthy
105
+ #
106
+ # @return [Boolean] True if Sentry is configured
107
+ def healthy?
108
+ ::Sentry.initialized?
109
+ end
110
+
111
+ private
112
+
113
+ # Validate configuration
114
+ def validate_config!
115
+ raise ArgumentError, "Sentry adapter requires :dsn" unless @dsn
116
+
117
+ return if SEVERITY_LEVELS.include?(@severity_threshold)
118
+
119
+ raise ArgumentError,
120
+ "Invalid severity_threshold: #{@severity_threshold}"
121
+ end
122
+
123
+ # Initialize Sentry SDK
124
+ def initialize_sentry!
125
+ ::Sentry.init do |config|
126
+ config.dsn = @dsn
127
+ config.environment = @environment
128
+ config.breadcrumbs_logger = [] # We manage breadcrumbs manually
129
+ end
130
+ end
131
+
132
+ # Check if severity should be sent to Sentry
133
+ #
134
+ # @param severity [Symbol] Event severity
135
+ # @return [Boolean] True if severity >= threshold
136
+ def should_send_to_sentry?(severity)
137
+ threshold_index = SEVERITY_LEVELS.index(@severity_threshold)
138
+ current_index = SEVERITY_LEVELS.index(severity)
139
+
140
+ return false unless threshold_index && current_index
141
+
142
+ current_index >= threshold_index
143
+ end
144
+
145
+ # Check if severity is error-level
146
+ #
147
+ # @param severity [Symbol] Event severity
148
+ # @return [Boolean] True if error or fatal
149
+ def error_severity?(severity)
150
+ %i[error fatal].include?(severity)
151
+ end
152
+
153
+ # Send error to Sentry
154
+ #
155
+ # @param event_data [Hash] Event data
156
+ def send_error_to_sentry(event_data)
157
+ ::Sentry.with_scope do |scope|
158
+ # Set tags
159
+ scope.set_tags(extract_tags(event_data))
160
+
161
+ # Set extras
162
+ scope.set_extras(event_data[:payload] || {})
163
+
164
+ # Set user context
165
+ scope.set_user(event_data[:user] || {}) if event_data[:user]
166
+
167
+ # Set trace context
168
+ if event_data[:trace_id]
169
+ scope.set_context("trace", {
170
+ trace_id: event_data[:trace_id],
171
+ span_id: event_data[:span_id]
172
+ })
173
+ end
174
+
175
+ # Capture exception or message
176
+ if event_data[:exception]
177
+ ::Sentry.capture_exception(event_data[:exception])
178
+ else
179
+ ::Sentry.capture_message(
180
+ event_data[:message] || event_data[:event_name].to_s,
181
+ level: sentry_level(event_data[:severity])
182
+ )
183
+ end
184
+ end
185
+ end
186
+
187
+ # Send breadcrumb to Sentry
188
+ #
189
+ # @param event_data [Hash] Event data
190
+ def send_breadcrumb_to_sentry(event_data)
191
+ ::Sentry.add_breadcrumb(
192
+ ::Sentry::Breadcrumb.new(
193
+ category: event_data[:event_name].to_s,
194
+ message: event_data[:message]&.to_s,
195
+ level: sentry_level(event_data[:severity]),
196
+ data: event_data[:payload] || {},
197
+ timestamp: event_data[:timestamp]&.to_i
198
+ )
199
+ )
200
+ end
201
+
202
+ # Extract tags from event
203
+ #
204
+ # @param event_data [Hash] Event data
205
+ # @return [Hash] Tags for Sentry
206
+ def extract_tags(event_data)
207
+ {
208
+ event_name: event_data[:event_name].to_s,
209
+ severity: event_data[:severity].to_s,
210
+ environment: @environment
211
+ }
212
+ end
213
+
214
+ # Map E11y severity to Sentry level
215
+ #
216
+ # @param severity [Symbol] E11y severity
217
+ # @return [Symbol] Sentry level
218
+ def sentry_level(severity)
219
+ case severity
220
+ when :debug then :debug
221
+ when :info, :success then :info
222
+ when :warn then :warning
223
+ when :error then :error
224
+ when :fatal then :fatal
225
+ else :info
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module E11y
6
+ module Adapters
7
+ # Stdout Adapter - Console output for development and debugging
8
+ #
9
+ # Outputs events to STDOUT with optional colorization and pretty printing.
10
+ # Primarily for development use.
11
+ #
12
+ # **Features:**
13
+ # - Colorized output based on severity
14
+ # - Pretty-print JSON (optional)
15
+ # - Streaming output
16
+ #
17
+ # @example Configuration
18
+ # E11y.configure do |config|
19
+ # config.register_adapter :stdout, E11y::Adapters::Stdout.new(
20
+ # colorize: true,
21
+ # pretty_print: true
22
+ # )
23
+ # end
24
+ #
25
+ # @see ADR-004 §4.1 (Stdout Adapter)
26
+ class Stdout < Base
27
+ # ANSI color codes for severity levels
28
+ SEVERITY_COLORS = {
29
+ debug: "\e[37m", # Gray
30
+ info: "\e[36m", # Cyan
31
+ success: "\e[32m", # Green
32
+ warn: "\e[33m", # Yellow
33
+ error: "\e[31m", # Red
34
+ fatal: "\e[35m" # Magenta
35
+ }.freeze
36
+
37
+ # Color reset
38
+ COLOR_RESET = "\e[0m"
39
+
40
+ # Initialize adapter
41
+ #
42
+ # @param config [Hash] Configuration options
43
+ # @option config [Boolean] :colorize (true) Enable colored output
44
+ # @option config [Boolean] :pretty_print (true) Enable pretty-printed JSON
45
+ def initialize(config = {})
46
+ @colorize = config.fetch(:colorize, true)
47
+ @pretty_print = config.fetch(:pretty_print, true)
48
+
49
+ super
50
+ end
51
+
52
+ # Write event to STDOUT
53
+ #
54
+ # @param event_data [Hash] Event payload
55
+ # @return [Boolean] true on success, false on failure
56
+ def write(event_data)
57
+ output = format_event(event_data)
58
+
59
+ if @colorize
60
+ puts colorize_output(output, event_data[:severity])
61
+ else
62
+ puts output
63
+ end
64
+
65
+ true
66
+ rescue StandardError => e
67
+ warn "Stdout adapter error: #{e.message}"
68
+ false
69
+ end
70
+
71
+ # Adapter capabilities
72
+ #
73
+ # @return [Hash] Capability flags
74
+ def capabilities
75
+ {
76
+ batching: false,
77
+ compression: false,
78
+ async: false,
79
+ streaming: true
80
+ }
81
+ end
82
+
83
+ private
84
+
85
+ # Format event for console output
86
+ #
87
+ # @param event_data [Hash] Event data
88
+ # @return [String] Formatted output
89
+ def format_event(event_data)
90
+ if @pretty_print
91
+ JSON.pretty_generate(event_data)
92
+ else
93
+ event_data.to_json
94
+ end
95
+ end
96
+
97
+ # Colorize output based on severity
98
+ #
99
+ # @param output [String] Formatted output
100
+ # @param severity [Symbol] Event severity
101
+ # @return [String] Colorized output
102
+ def colorize_output(output, severity)
103
+ color_code = SEVERITY_COLORS[severity] || ""
104
+ "#{color_code}#{output}#{COLOR_RESET}"
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,370 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/adapters/base"
4
+ require "e11y/metrics/cardinality_protection"
5
+ require "e11y/metrics/registry"
6
+
7
+ # Check if Yabeda is available
8
+ begin
9
+ require "yabeda"
10
+ rescue LoadError
11
+ raise LoadError, <<~ERROR
12
+ Yabeda not available!
13
+
14
+ To use E11y::Adapters::Yabeda, add to your Gemfile:
15
+
16
+ gem 'yabeda'
17
+ gem 'yabeda-prometheus' # For Prometheus exporter
18
+
19
+ Then run: bundle install
20
+ ERROR
21
+ end
22
+
23
+ module E11y
24
+ module Adapters
25
+ # Yabeda adapter for E11y metrics.
26
+ #
27
+ # This adapter integrates with Yabeda to expose metrics to Prometheus.
28
+ # It includes built-in cardinality protection to prevent metric explosions.
29
+ #
30
+ # Features:
31
+ # - Automatic metric registration from E11y::Metrics::Registry
32
+ # - 3-layer cardinality protection (denylist, per-metric limits, monitoring)
33
+ # - Counter, Histogram, and Gauge support
34
+ # - Thread-safe metric updates
35
+ #
36
+ # @example Basic usage
37
+ # adapter = E11y::Adapters::Yabeda.new(
38
+ # cardinality_limit: 1000,
39
+ # forbidden_labels: [:custom_id]
40
+ # )
41
+ #
42
+ # # Metrics are automatically registered from Registry
43
+ # # Events automatically update metrics via middleware
44
+ #
45
+ # @see ADR-002 Metrics & Yabeda Integration
46
+ # @see UC-003 Pattern-Based Metrics
47
+ class Yabeda < Base
48
+ # Initialize Yabeda adapter
49
+ #
50
+ # @param config [Hash] Configuration options
51
+ # @option config [Integer] :cardinality_limit (1000) Max unique values per label per metric
52
+ # @option config [Array<Symbol>] :forbidden_labels ([]) Additional labels to denylist
53
+ # @option config [Boolean] :auto_register (true) Automatically register metrics from Registry
54
+ def initialize(config = {})
55
+ super
56
+
57
+ @cardinality_protection = E11y::Metrics::CardinalityProtection.new(
58
+ cardinality_limit: config.fetch(:cardinality_limit, 1000),
59
+ forbidden_labels: config.fetch(:forbidden_labels, [])
60
+ )
61
+
62
+ # Auto-register metrics from Registry
63
+ register_metrics_from_registry! if config.fetch(:auto_register, true)
64
+ end
65
+
66
+ # Write a single event to Yabeda
67
+ #
68
+ # Extracts metrics from event data and updates corresponding Yabeda metrics.
69
+ # Applies cardinality protection to prevent label explosions.
70
+ #
71
+ # @param event_data [Hash] Event data
72
+ # @return [Boolean] true if successful
73
+ def write(event_data)
74
+ event_name = event_data[:event_name].to_s
75
+ matching_metrics = E11y::Metrics::Registry.instance.find_matching(event_name)
76
+
77
+ matching_metrics.each do |metric_config|
78
+ update_metric(metric_config, event_data)
79
+ end
80
+
81
+ true
82
+ rescue StandardError => e
83
+ warn "E11y Yabeda adapter error: #{e.message}"
84
+ false
85
+ end
86
+
87
+ # Write a batch of events
88
+ #
89
+ # @param events [Array<Hash>] Array of event data hashes
90
+ # @return [Boolean] true if successful
91
+ def write_batch(events)
92
+ events.each { |event| write(event) }
93
+ true
94
+ rescue StandardError => e
95
+ warn "E11y Yabeda adapter batch error: #{e.message}"
96
+ false
97
+ end
98
+
99
+ # Check if adapter is healthy
100
+ #
101
+ # @return [Boolean] true if Yabeda is available and configured
102
+ def healthy?
103
+ return false unless defined?(::Yabeda)
104
+
105
+ ::Yabeda.configured?
106
+ rescue StandardError
107
+ false
108
+ end
109
+
110
+ # Close adapter (no-op for Yabeda)
111
+ #
112
+ # @return [void]
113
+ def close
114
+ # Yabeda doesn't need explicit cleanup
115
+ end
116
+
117
+ # Get adapter capabilities
118
+ #
119
+ # @return [Hash] Capabilities hash
120
+ def capabilities
121
+ {
122
+ batch: true,
123
+ async: false,
124
+ filtering: false,
125
+ metrics: true
126
+ }
127
+ end
128
+
129
+ # Track a counter metric (for E11y::Metrics facade).
130
+ #
131
+ # @param name [Symbol] Metric name
132
+ # @param labels [Hash] Metric labels
133
+ # @param value [Integer] Increment value (default: 1)
134
+ # @return [void]
135
+ def increment(name, labels = {}, value: 1)
136
+ return unless healthy?
137
+
138
+ # Apply cardinality protection
139
+ safe_labels = @cardinality_protection.filter(labels, name)
140
+
141
+ # Register metric if not exists
142
+ register_metric_if_needed(name, :counter, safe_labels.keys)
143
+
144
+ # Update Yabeda metric
145
+ ::Yabeda.e11y.send(name).increment(safe_labels, by: value)
146
+ rescue StandardError => e
147
+ E11y.logger.warn("Failed to increment Yabeda metric #{name}: #{e.message}", error: e.class.name)
148
+ end
149
+
150
+ # Track a histogram metric (for E11y::Metrics facade).
151
+ #
152
+ # @param name [Symbol] Metric name
153
+ # @param value [Numeric] Observed value
154
+ # @param labels [Hash] Metric labels
155
+ # @param buckets [Array<Numeric>, nil] Optional histogram buckets
156
+ # @return [void]
157
+ def histogram(name, value, labels = {}, buckets: nil)
158
+ return unless healthy?
159
+
160
+ # Apply cardinality protection
161
+ safe_labels = @cardinality_protection.filter(labels, name)
162
+
163
+ # Register metric if not exists
164
+ register_metric_if_needed(name, :histogram, safe_labels.keys, buckets: buckets)
165
+
166
+ # Update Yabeda metric
167
+ ::Yabeda.e11y.send(name).observe(value, safe_labels)
168
+ rescue StandardError => e
169
+ E11y.logger.warn("Failed to observe Yabeda histogram #{name}: #{e.message}", error: e.class.name)
170
+ end
171
+
172
+ # Track a gauge metric (for E11y::Metrics facade).
173
+ #
174
+ # @param name [Symbol] Metric name
175
+ # @param value [Numeric] Current value
176
+ # @param labels [Hash] Metric labels
177
+ # @return [void]
178
+ def gauge(name, value, labels = {})
179
+ return unless healthy?
180
+
181
+ # Apply cardinality protection
182
+ safe_labels = @cardinality_protection.filter(labels, name)
183
+
184
+ # Register metric if not exists
185
+ register_metric_if_needed(name, :gauge, safe_labels.keys)
186
+
187
+ # Update Yabeda metric
188
+ ::Yabeda.e11y.send(name).set(value, safe_labels)
189
+ rescue StandardError => e
190
+ E11y.logger.warn("Failed to set Yabeda gauge #{name}: #{e.message}", error: e.class.name)
191
+ end
192
+
193
+ # Validate configuration
194
+ #
195
+ # @raise [ArgumentError] if configuration is invalid
196
+ # @return [void]
197
+ def validate_config!
198
+ super
199
+
200
+ # Validate cardinality_limit
201
+ if @config[:cardinality_limit] && !@config[:cardinality_limit].is_a?(Integer)
202
+ raise ArgumentError, "cardinality_limit must be an Integer"
203
+ end
204
+
205
+ # Validate forbidden_labels
206
+ return unless @config[:forbidden_labels] && !@config[:forbidden_labels].is_a?(Array)
207
+
208
+ raise ArgumentError, "forbidden_labels must be an Array"
209
+ end
210
+
211
+ # Format event for Yabeda (no-op, metrics are updated directly)
212
+ #
213
+ # @param event_data [Hash] Event data
214
+ # @return [Hash] Original event data
215
+ def format_event(event_data)
216
+ event_data
217
+ end
218
+
219
+ # Get current cardinality statistics
220
+ #
221
+ # @return [Hash] Cardinality statistics per metric:label
222
+ def cardinality_stats
223
+ @cardinality_protection.cardinalities
224
+ end
225
+
226
+ # Reset cardinality tracking (for testing)
227
+ #
228
+ # @return [void]
229
+ def reset_cardinality!
230
+ @cardinality_protection.reset!
231
+ end
232
+
233
+ private
234
+
235
+ # Register metrics from Registry into Yabeda
236
+ #
237
+ # This is called during initialization if auto_register is true.
238
+ # It creates Yabeda metric definitions for all metrics in the Registry.
239
+ #
240
+ # @return [void]
241
+ def register_metrics_from_registry!
242
+ return unless defined?(::Yabeda)
243
+
244
+ registry = E11y::Metrics::Registry.instance
245
+ registry.all.each do |metric_config|
246
+ register_yabeda_metric(metric_config)
247
+ end
248
+ end
249
+
250
+ # Register a single metric in Yabeda
251
+ #
252
+ # @param metric_config [Hash] Metric configuration from Registry
253
+ # @return [void]
254
+ def register_yabeda_metric(metric_config)
255
+ metric_name = metric_config[:name]
256
+ metric_type = metric_config[:type]
257
+ tags = metric_config[:tags] || []
258
+
259
+ # Define metric in Yabeda group
260
+ ::Yabeda.configure do
261
+ group :e11y do
262
+ case metric_type
263
+ when :counter
264
+ counter metric_name, tags: tags, comment: "E11y metric: #{metric_name}"
265
+ when :histogram
266
+ histogram metric_name,
267
+ tags: tags,
268
+ buckets: metric_config[:buckets] || [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10],
269
+ comment: "E11y metric: #{metric_name}"
270
+ when :gauge
271
+ gauge metric_name, tags: tags, comment: "E11y metric: #{metric_name}"
272
+ end
273
+ end
274
+ end
275
+ rescue StandardError => e
276
+ # Metric might already be registered - that's OK
277
+ warn "E11y Yabeda: Could not register metric #{metric_name}: #{e.message}"
278
+ end
279
+
280
+ # Register a metric if it doesn't exist yet (for direct metric calls).
281
+ #
282
+ # @param name [Symbol] Metric name
283
+ # @param type [Symbol] Metric type (:counter, :histogram, :gauge)
284
+ # @param tags [Array<Symbol>] Metric tags (labels)
285
+ # @param buckets [Array<Numeric>, nil] Optional histogram buckets
286
+ # @return [void]
287
+ # @api private
288
+ def register_metric_if_needed(name, type, tags, buckets: nil)
289
+ # Check if metric already exists
290
+ return if ::Yabeda.metrics.key?(:"e11y_#{name}")
291
+
292
+ ::Yabeda.configure do
293
+ group :e11y do
294
+ case type
295
+ when :counter
296
+ counter name, tags: tags, comment: "E11y self-monitoring: #{name}"
297
+ when :histogram
298
+ histogram name,
299
+ tags: tags,
300
+ buckets: buckets || [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10],
301
+ comment: "E11y self-monitoring: #{name}"
302
+ when :gauge
303
+ gauge name, tags: tags, comment: "E11y self-monitoring: #{name}"
304
+ end
305
+ end
306
+ end
307
+ rescue StandardError => e
308
+ # Metric might already be registered - that's OK
309
+ E11y.logger.debug("Could not register Yabeda metric #{name}: #{e.message}")
310
+ end
311
+
312
+ # Update a single metric based on event data
313
+ #
314
+ # @param metric_config [Hash] Metric configuration
315
+ # @param event_data [Hash] Event data
316
+ # @return [void]
317
+ def update_metric(metric_config, event_data)
318
+ metric_name = metric_config[:name]
319
+ labels = extract_labels(metric_config, event_data)
320
+
321
+ # Apply cardinality protection
322
+ safe_labels = @cardinality_protection.filter(labels, metric_name)
323
+
324
+ # Extract value for histogram/gauge
325
+ value = extract_value(metric_config, event_data) if %i[histogram gauge].include?(metric_config[:type])
326
+
327
+ # Update Yabeda metric
328
+ case metric_config[:type]
329
+ when :counter
330
+ ::Yabeda.e11y.send(metric_name).increment(safe_labels)
331
+ when :histogram
332
+ ::Yabeda.e11y.send(metric_name).observe(value, safe_labels)
333
+ when :gauge
334
+ ::Yabeda.e11y.send(metric_name).set(value, safe_labels)
335
+ end
336
+ rescue StandardError => e
337
+ warn "E11y Yabeda: Error updating metric #{metric_name}: #{e.message}"
338
+ end
339
+
340
+ # Extract labels from event data
341
+ #
342
+ # @param metric_config [Hash] Metric configuration
343
+ # @param event_data [Hash] Event data
344
+ # @return [Hash] Extracted labels
345
+ def extract_labels(metric_config, event_data)
346
+ metric_config.fetch(:tags, []).each_with_object({}) do |tag, acc|
347
+ value = event_data[tag] || event_data.dig(:payload, tag)
348
+ acc[tag] = value.to_s if value
349
+ end
350
+ end
351
+
352
+ # Extract value for histogram or gauge metrics
353
+ #
354
+ # @param metric_config [Hash] Metric configuration
355
+ # @param event_data [Hash] Event data
356
+ # @return [Numeric] The extracted value
357
+ def extract_value(metric_config, event_data)
358
+ value_extractor = metric_config[:value]
359
+ case value_extractor
360
+ when Symbol
361
+ event_data[value_extractor] || event_data.dig(:payload, value_extractor)
362
+ when Proc
363
+ value_extractor.call(event_data)
364
+ else
365
+ 1 # Default fallback
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end