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