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,1911 @@
1
+ # ADR-008: Rails Integration
2
+
3
+ **Status:** Draft
4
+ **Date:** January 12, 2026
5
+ **Covers:** UC-010 (Background Job Tracking), UC-016 (Rails Logger Migration)
6
+ **Depends On:** ADR-001 (Core), ADR-004 (Adapters), ADR-006 (Security)
7
+
8
+ ---
9
+
10
+ ## 📋 Table of Contents
11
+
12
+ 1. [Context & Problem](#1-context--problem)
13
+ 2. [Architecture Overview](#2-architecture-overview)
14
+ 3. [Railtie & Initialization](#3-railtie--initialization)
15
+ 4. [ActiveSupport::Notifications Integration](#4-activesupportnotifications-integration)
16
+ 5. [Sidekiq Integration](#5-sidekiq-integration)
17
+ 6. [ActiveJob Integration](#6-activejob-integration)
18
+ 7. [Rails.logger Migration](#7-railslogger-migration)
19
+ 8. [Middleware Integration](#8-middleware-integration)
20
+ 9. [Console & Development](#9-console--development)
21
+ 10. [Testing in Rails](#10-testing-in-rails)
22
+ 11. [Trade-offs](#11-trade-offs)
23
+
24
+ ---
25
+
26
+ ## 1. Context & Problem
27
+
28
+ ### 1.1. Problem Statement
29
+
30
+ **Current Pain Points:**
31
+
32
+ 1. **Manual Instrumentation:**
33
+ ```ruby
34
+ # ❌ Manual tracking everywhere
35
+ def create
36
+ Events::OrderCreated.track(order_id: order.id)
37
+ order.save
38
+ end
39
+ ```
40
+
41
+ 2. **No Rails Integration:**
42
+ - Can't leverage ActiveSupport::Notifications
43
+ - No automatic Sidekiq/ActiveJob tracking
44
+ - No Rails.logger compatibility
45
+
46
+ 3. **Complex Setup:**
47
+ ```ruby
48
+ # ❌ Boilerplate in every Rails app
49
+ config/initializers/e11y.rb # Manual setup
50
+ app/events/... # Manual event definitions
51
+ config/environments/*.rb # Environment-specific config
52
+ ```
53
+
54
+ ### 1.2. Goals
55
+
56
+ **Primary Goals:**
57
+ - ✅ Zero-config Rails integration via Railtie
58
+ - ✅ Auto-instrument Sidekiq & ActiveJob
59
+ - ✅ Bidirectional ActiveSupport::Notifications
60
+ - ✅ Drop-in Rails.logger replacement
61
+ - ✅ Development-friendly (console, debugging)
62
+
63
+ **Non-Goals:**
64
+ - ❌ Support non-Rails Ruby apps (Rails 8.0+ only)
65
+ - ❌ Backwards compatibility with Rails < 8.0
66
+ - ❌ Auto-instrument every possible gem
67
+
68
+ ### 1.3. Success Metrics
69
+
70
+ | Metric | Target | Critical? |
71
+ |--------|--------|-----------|
72
+ | **Setup time** | <5 minutes | ✅ Yes |
73
+ | **Auto-instrumentation coverage** | >80% Rails events | ✅ Yes |
74
+ | **Performance overhead** | <5% | ✅ Yes |
75
+ | **Rails.logger compatibility** | 100% | ✅ Yes |
76
+
77
+ ---
78
+
79
+ ## 2. Architecture Overview
80
+
81
+ ### 2.1. System Context
82
+
83
+ ```mermaid
84
+ C4Context
85
+ title Rails Integration Context
86
+
87
+ Person(dev, "Rails Developer", "Builds Rails app")
88
+
89
+ System(rails, "Rails Application", "Web app with E11y")
90
+
91
+ System_Ext(sidekiq, "Sidekiq", "Background jobs")
92
+ System_Ext(activejob, "ActiveJob", "Job framework")
93
+ System_Ext(puma, "Puma", "Web server")
94
+
95
+ Rel(dev, rails, "Develops", "Rails code")
96
+ Rel(rails, sidekiq, "Enqueues jobs", "Sidekiq API")
97
+ Rel(rails, activejob, "Uses", "ActiveJob API")
98
+ Rel(puma, rails, "Serves requests", "Rack")
99
+
100
+ System(e11y, "E11y Gem", "Auto-instruments Rails")
101
+
102
+ Rel(e11y, rails, "Hooks into", "Railtie, Middleware, Notifications")
103
+ Rel(e11y, sidekiq, "Instruments", "Sidekiq Middleware")
104
+ Rel(e11y, activejob, "Instruments", "ActiveJob Callbacks")
105
+ ```
106
+
107
+ ### 2.2. Component Architecture
108
+
109
+ ```mermaid
110
+ graph TB
111
+ subgraph "Rails Boot Process"
112
+ Boot[Rails Boot] --> Railtie[E11y::Railtie]
113
+ Railtie --> Init[Initialize E11y]
114
+ Init --> Config[Load Config]
115
+ Config --> Instruments[Setup Instruments]
116
+ end
117
+
118
+ subgraph "Instrumentation Layer"
119
+ Instruments --> ASN[ActiveSupport::Notifications]
120
+ Instruments --> SidekiqMiddleware[Sidekiq Middleware]
121
+ Instruments --> ActiveJobHooks[ActiveJob Callbacks]
122
+ Instruments --> RackMiddleware[Rack Middleware]
123
+ end
124
+
125
+ subgraph "E11y Core"
126
+ ASN --> E11yPipeline[E11y Pipeline]
127
+ SidekiqMiddleware --> E11yPipeline
128
+ ActiveJobHooks --> E11yPipeline
129
+ RackMiddleware --> E11yPipeline
130
+ end
131
+
132
+ subgraph "Rails Logger Bridge"
133
+ RailsLogger[Rails.logger] --> LoggerBridge[E11y Logger Bridge]
134
+ LoggerBridge --> E11yPipeline
135
+ end
136
+
137
+ style Railtie fill:#d1ecf1
138
+ style ASN fill:#fff3cd
139
+ style E11yPipeline fill:#d4edda
140
+ ```
141
+
142
+ ### 2.3. Request Lifecycle
143
+
144
+ ```mermaid
145
+ sequenceDiagram
146
+ participant Browser as Browser
147
+ participant Rack as Rack Middleware
148
+ participant Rails as Rails App
149
+ participant E11y as E11y
150
+ participant Sidekiq as Sidekiq
151
+ participant Adapters as Adapters
152
+
153
+ Browser->>Rack: GET /orders/123
154
+
155
+ Rack->>E11y: start_request (middleware)
156
+ E11y->>E11y: Create trace_id
157
+ E11y->>E11y: Start request-scoped buffer
158
+
159
+ Rack->>Rails: Handle request
160
+
161
+ Rails->>E11y: Track event (OrderViewed)
162
+ E11y->>E11y: Buffer event
163
+
164
+ Rails->>Sidekiq: Enqueue job (SendOrderEmail)
165
+ Sidekiq->>E11y: job.enqueued (ActiveJob hook)
166
+ E11y->>E11y: Propagate trace_id to job
167
+
168
+ Rails-->>Rack: 200 OK
169
+
170
+ Rack->>E11y: end_request (middleware)
171
+ E11y->>E11y: Flush buffers
172
+ E11y->>Adapters: Send batched events
173
+
174
+ Note over Sidekiq: Job executes later
175
+ Sidekiq->>E11y: job.perform_start
176
+ E11y->>E11y: Restore trace_id
177
+ Sidekiq->>E11y: job.perform
178
+ E11y->>E11y: Track job events
179
+ Sidekiq->>E11y: job.perform_end
180
+ E11y->>Adapters: Flush job events
181
+ ```
182
+
183
+ ---
184
+
185
+ ## 3. Railtie & Initialization
186
+
187
+ ### 3.1. Railtie Implementation
188
+
189
+ ```ruby
190
+ # lib/e11y/railtie.rb
191
+ module E11y
192
+ class Railtie < Rails::Railtie
193
+ # Run before framework initialization
194
+ config.before_initialize do
195
+ # Set up basic configuration
196
+ E11y.configure do |config|
197
+ config.environment = Rails.env
198
+ config.service_name = Rails.application.class.module_parent_name.underscore
199
+ end
200
+ end
201
+
202
+ # Run after framework initialization
203
+ config.after_initialize do
204
+ next unless E11y.config.enabled
205
+
206
+ # Setup instruments (each can be enabled/disabled separately)
207
+ if E11y.config.instruments.active_support_notifications.enabled
208
+ E11y::Instruments::ActiveSupportNotifications.setup!
209
+ end
210
+
211
+ if E11y.config.instruments.sidekiq.enabled
212
+ E11y::Instruments::Sidekiq.setup!
213
+ end
214
+
215
+ if E11y.config.instruments.active_job.enabled
216
+ E11y::Instruments::ActiveJob.setup!
217
+ end
218
+
219
+ if E11y.config.instruments.rack_middleware.enabled
220
+ E11y::Instruments::RackMiddleware.setup!
221
+ end
222
+
223
+ # Setup logger bridge
224
+ E11y::Logger::Bridge.setup! if E11y.config.logger_bridge.enabled
225
+
226
+ # Setup development tools
227
+ E11y::Console.setup! if Rails.env.development?
228
+ end
229
+
230
+ # Middleware insertion
231
+ initializer 'e11y.middleware' do |app|
232
+ app.middleware.insert_before(
233
+ Rails::Rack::Logger,
234
+ E11y::Middleware::Request
235
+ )
236
+ end
237
+
238
+ # ActiveSupport::Notifications subscribers
239
+ initializer 'e11y.notifications' do
240
+ ActiveSupport::Notifications.subscribe(/.*/) do |name, start, finish, id, payload|
241
+ E11y::Instruments::NotificationSubscriber.handle(
242
+ name: name,
243
+ started_at: start,
244
+ finished_at: finish,
245
+ transaction_id: id,
246
+ payload: payload
247
+ )
248
+ end
249
+ end
250
+
251
+ # Sidekiq integration
252
+ initializer 'e11y.sidekiq' do
253
+ if defined?(Sidekiq)
254
+ require 'e11y/instruments/sidekiq'
255
+
256
+ Sidekiq.configure_server do |config|
257
+ config.server_middleware do |chain|
258
+ chain.add E11y::Instruments::Sidekiq::ServerMiddleware
259
+ end
260
+ end
261
+
262
+ Sidekiq.configure_client do |config|
263
+ config.client_middleware do |chain|
264
+ chain.add E11y::Instruments::Sidekiq::ClientMiddleware
265
+ end
266
+ end
267
+ end
268
+ end
269
+
270
+ # ActiveJob integration
271
+ initializer 'e11y.active_job' do
272
+ ActiveSupport.on_load(:active_job) do
273
+ require 'e11y/instruments/active_job'
274
+
275
+ include E11y::Instruments::ActiveJob::Callbacks
276
+ end
277
+ end
278
+
279
+ # Console helpers
280
+ console do
281
+ E11y::Console.enable!
282
+
283
+ puts "E11y loaded. Try: E11y.stats"
284
+ end
285
+
286
+ # Rake task helpers
287
+ rake_tasks do
288
+ load 'e11y/tasks.rake'
289
+ end
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### 3.2. Configuration Loading
295
+
296
+ ```ruby
297
+ # lib/e11y/configuration/rails.rb
298
+ module E11y
299
+ module Configuration
300
+ class Rails < Base
301
+ def initialize
302
+ super
303
+
304
+ # Rails-specific defaults
305
+ @environment = ::Rails.env
306
+ @service_name = derive_service_name
307
+ @enabled = !::Rails.env.test? # Disabled in tests by default
308
+
309
+ # Auto-detect adapters
310
+ @adapters = auto_detect_adapters
311
+
312
+ # Rails logger bridge
313
+ @logger_bridge = LoggerBridgeConfig.new
314
+ end
315
+
316
+ private
317
+
318
+ def derive_service_name
319
+ ::Rails.application.class.module_parent_name.underscore
320
+ rescue
321
+ 'rails_app'
322
+ end
323
+
324
+ def auto_detect_adapters
325
+ adapters = []
326
+
327
+ # Always include stdout in development
328
+ adapters << :stdout if ::Rails.env.development?
329
+
330
+ # Auto-detect file logging
331
+ adapters << :file if ::Rails.root.join('log').directory?
332
+
333
+ # Auto-detect Sentry
334
+ adapters << :sentry if defined?(Sentry)
335
+
336
+ # Auto-detect Loki
337
+ adapters << :loki if ENV['LOKI_URL'].present?
338
+
339
+ adapters
340
+ end
341
+ end
342
+ end
343
+ end
344
+ ```
345
+
346
+ ---
347
+
348
+ ## 4. ActiveSupport::Notifications Integration
349
+
350
+ ### 4.1. Unidirectional Flow (ASN → E11y)
351
+
352
+ **Design Decision (Updated 2026-01-17):** **Unidirectional** flow from ActiveSupport::Notifications to E11y.
353
+
354
+ **Rationale:**
355
+ - ✅ **Avoids infinite loops**: Bidirectional bridge can create cycles (E11y → ASN → E11y → ...)
356
+ - ✅ **Simpler reasoning**: Single direction = clear data flow
357
+ - ✅ **Better performance**: No double overhead of publish + subscribe
358
+ - ✅ **Separation of concerns**: ASN = Rails instrumentation, E11y = Business events + adapters
359
+
360
+ **Architecture:**
361
+ ```
362
+ ┌─────────────────────────────────────────────────────────────────┐
363
+ │ Rails Application │
364
+ ├─────────────────────────────────────────────────────────────────┤
365
+ │ │
366
+ │ ActiveSupport::Notifications │
367
+ │ ┌──────────────────────────────────────────────────┐ │
368
+ │ │ Rails Internal Events: │ │
369
+ │ │ - sql.active_record │ │
370
+ │ │ - process_action.action_controller │ │
371
+ │ │ - render_template.action_view │ │
372
+ │ │ - enqueue.active_job │ │
373
+ │ └──────────────────────────────────────────────────┘ │
374
+ │ │ │
375
+ │ │ SUBSCRIBE ONLY (Unidirectional) │
376
+ │ ▼ │
377
+ │ ┌──────────────────────────────────────────────────┐ │
378
+ │ │ E11y::Instruments::RailsInstrumentation │ │
379
+ │ │ - Convert ASN events → E11y events │ │
380
+ │ │ - Apply event mapping (overridable!) │ │
381
+ │ │ - Route to E11y pipeline │ │
382
+ │ └──────────────────────────────────────────────────┘ │
383
+ │ │ │
384
+ │ ▼ │
385
+ │ ┌──────────────────────────────────────────────────┐ │
386
+ │ │ E11y Event Pipeline │ │
387
+ │ │ → Middleware → Adapters → Loki/Sentry/etc │ │
388
+ │ └──────────────────────────────────────────────────┘ │
389
+ │ │
390
+ └─────────────────────────────────────────────────────────────────┘
391
+ ```
392
+
393
+ ```ruby
394
+ # lib/e11y/instruments/rails_instrumentation.rb
395
+ module E11y
396
+ module Instruments
397
+ class RailsInstrumentation
398
+ # ========================================
399
+ # ONLY ActiveSupport::Notifications → E11y
400
+ # ========================================
401
+
402
+ def self.setup!
403
+ return unless E11y.config.instruments.rails_instrumentation.enabled
404
+
405
+ # Subscribe to Rails events
406
+ event_mapping.each do |asn_pattern, e11y_event_class|
407
+ next if ignored?(asn_pattern)
408
+
409
+ ActiveSupport::Notifications.subscribe(asn_pattern) do |name, start, finish, id, payload|
410
+ duration = (finish - start) * 1000 # Convert to ms
411
+
412
+ # Convert ASN event → E11y event
413
+ e11y_event_class.track(
414
+ event_name: name,
415
+ duration: duration,
416
+ **extract_relevant_payload(payload)
417
+ )
418
+ end
419
+ end
420
+ end
421
+
422
+ # Built-in event mappings (can be overridden in config!)
423
+ DEFAULT_RAILS_EVENT_MAPPING = {
424
+ 'sql.active_record' => Events::Rails::Database::Query,
425
+ 'process_action.action_controller' => Events::Rails::Http::Request,
426
+ 'render_template.action_view' => Events::Rails::View::Render,
427
+ 'send_file.action_controller' => Events::Rails::Http::SendFile,
428
+ 'redirect_to.action_controller' => Events::Rails::Http::Redirect,
429
+ 'start_processing.action_controller' => Events::Rails::Http::StartProcessing,
430
+ 'cache_read.active_support' => Events::Rails::Cache::Read,
431
+ 'cache_write.active_support' => Events::Rails::Cache::Write,
432
+ 'cache_delete.active_support' => Events::Rails::Cache::Delete,
433
+ 'enqueue.active_job' => Events::Rails::Job::Enqueued,
434
+ 'enqueue_at.active_job' => Events::Rails::Job::Scheduled,
435
+ 'perform_start.active_job' => Events::Rails::Job::Started,
436
+ 'perform.active_job' => Events::Rails::Job::Completed
437
+ }.freeze
438
+
439
+ # Get final event mapping (after config overrides)
440
+ def self.event_mapping
441
+ @event_mapping ||= begin
442
+ mapping = DEFAULT_RAILS_EVENT_MAPPING.dup
443
+
444
+ # Apply custom mappings from config (Devise-style overrides)
445
+ custom_mappings = E11y.config.instruments.rails_instrumentation.custom_mappings || {}
446
+ mapping.merge!(custom_mappings)
447
+
448
+ mapping
449
+ end
450
+ end
451
+
452
+ def self.ignored?(pattern)
453
+ ignore_list = E11y.config.instruments.rails_instrumentation.ignore_events || []
454
+ ignore_list.include?(pattern)
455
+ end
456
+
457
+ def self.extract_relevant_payload(payload)
458
+ # Extract only relevant fields (avoid PII, reduce noise)
459
+ # Implementation depends on event type
460
+ payload.slice(:controller, :action, :format, :status, :allocations, :db_runtime)
461
+ end
462
+ end
463
+ end
464
+ end
465
+ ```
466
+
467
+ ### 4.2. Configuration: Overridable Event Classes (Devise-Style)
468
+
469
+ **Design Decision (Updated 2026-01-17):** Built-in event classes can be overridden in config (Devise-style pattern).
470
+
471
+ **Rationale:**
472
+ - ✅ **Flexibility**: Custom schema, PII rules, adapters per event type
473
+ - ✅ **Familiar pattern**: Developers know Devise controller overrides
474
+ - ✅ **Opt-in**: Defaults work for 90% of cases, override only when needed
475
+ - ✅ **No monkey-patching**: Clean override mechanism
476
+
477
+ ```ruby
478
+ # config/initializers/e11y.rb
479
+ E11y.configure do |config|
480
+ config.instruments do
481
+ # ========================================
482
+ # Rails Instrumentation (ActiveSupport::Notifications → E11y)
483
+ # ========================================
484
+ rails_instrumentation do
485
+ # Enable/disable entire ASN integration
486
+ enabled true # Set to false to completely disable
487
+
488
+ # Built-in event classes (auto-created by E11y)
489
+ # Located in Events::Rails namespace
490
+ use_built_in_events true # If false, no auto-mapping
491
+
492
+ # ========================================
493
+ # OVERRIDE EVENT CLASSES (Devise-style)
494
+ # ========================================
495
+
496
+ # Override default event class for specific ASN pattern
497
+ event_class_for 'sql.active_record', MyApp::Events::CustomDatabaseQuery
498
+ event_class_for 'process_action.action_controller', MyApp::Events::CustomHttpRequest
499
+
500
+ # Disable specific events (too noisy or not needed)
501
+ ignore_event 'cache_read.active_support'
502
+ ignore_event 'render_partial.action_view'
503
+ ignore_event 'SCHEMA' # Schema queries
504
+
505
+ # ========================================
506
+ # SELECTIVE INSTRUMENTATION
507
+ # ========================================
508
+
509
+ # Which Rails events to track (glob patterns)
510
+ track_patterns [
511
+ 'sql.active_record',
512
+ 'process_action.action_controller',
513
+ 'render_template.action_view',
514
+ 'cache_*.active_support'
515
+ ]
516
+
517
+ # Sampling for high-volume events
518
+ sample_patterns do
519
+ pattern 'sql.active_record', sample_rate: 0.1 # 10% of SQL queries
520
+ pattern 'cache_read.active_support', sample_rate: 0.01 # 1% of cache reads
521
+ end
522
+
523
+ # Enrich with custom data
524
+ enrich do |asn_event|
525
+ {
526
+ controller: asn_event.payload[:controller],
527
+ action: asn_event.payload[:action],
528
+ format: asn_event.payload[:format],
529
+ user_id: Current.user&.id # Add context
530
+ }
531
+ end
532
+ end
533
+
534
+ # (Other integrations: Sidekiq, ActiveJob, etc.)
535
+ end
536
+ end
537
+ ```
538
+
539
+ ### 4.2.1. Custom Event Class Example
540
+
541
+ **Use Case:** Override default `Events::Rails::Database::Query` with custom schema and PII rules.
542
+
543
+ ```ruby
544
+ # app/events/custom_database_query.rb
545
+ module MyApp
546
+ module Events
547
+ class CustomDatabaseQuery < E11y::Event::Base
548
+ schema do
549
+ required(:query).filled(:string)
550
+ required(:duration).filled(:float)
551
+ required(:connection_name).filled(:string)
552
+
553
+ # Custom field (not in default Event::Rails::Database::Query)
554
+ optional(:database_shard).filled(:string)
555
+ optional(:query_type).filled(:string) # SELECT, INSERT, UPDATE, etc.
556
+ end
557
+
558
+ # Custom severity (mark slow queries as warnings)
559
+ severity do |payload|
560
+ payload[:duration] > 1000 ? :warn : :debug
561
+ end
562
+
563
+ # Custom adapter routing (also send to Elasticsearch)
564
+ adapters [:loki, :elasticsearch]
565
+
566
+ # Custom PII filtering (more aggressive than default)
567
+ pii_filtering do
568
+ masks :query # Mask entire SQL query
569
+ hashes :connection_name # Hash connection name
570
+ end
571
+
572
+ # Custom rate limiting (protect from query floods)
573
+ rate_limit 100, window: 1.second
574
+
575
+ # Custom retention (keep only for 7 days)
576
+ retention 7.days
577
+ end
578
+ end
579
+ end
580
+
581
+ # config/initializers/e11y.rb
582
+ E11y.configure do |config|
583
+ config.instruments.rails_instrumentation do
584
+ # Override default event class
585
+ event_class_for 'sql.active_record', MyApp::Events::CustomDatabaseQuery
586
+ end
587
+ end
588
+
589
+ # Now all sql.active_record events will use CustomDatabaseQuery!
590
+ ```
591
+
592
+ ### 4.2.2. Implementation (Configuration DSL)
593
+
594
+ ```ruby
595
+ # lib/e11y/configuration/rails_instrumentation.rb
596
+ module E11y
597
+ module Configuration
598
+ class RailsInstrumentation
599
+ attr_accessor :enabled, :use_built_in_events
600
+
601
+ def initialize
602
+ @enabled = true
603
+ @use_built_in_events = true
604
+ @custom_event_classes = {}
605
+ @ignored_events = []
606
+ @track_patterns = []
607
+ @sample_patterns = {}
608
+ @enrich_block = nil
609
+ end
610
+
611
+ # Override event class (Devise-style)
612
+ def event_class_for(asn_pattern, custom_event_class)
613
+ unless custom_event_class < E11y::Event::Base
614
+ raise ArgumentError, "#{custom_event_class} must inherit from E11y::Event::Base"
615
+ end
616
+
617
+ @custom_event_classes[asn_pattern] = custom_event_class
618
+ end
619
+
620
+ # Disable specific event
621
+ def ignore_event(asn_pattern)
622
+ @ignored_events << asn_pattern
623
+ end
624
+
625
+ # Track only specific patterns
626
+ def track_patterns(*patterns)
627
+ @track_patterns = patterns.flatten
628
+ end
629
+
630
+ # Sampling configuration
631
+ def sample_patterns(&block)
632
+ @sample_patterns_builder ||= SamplePatternsBuilder.new
633
+ @sample_patterns_builder.instance_eval(&block) if block_given?
634
+ @sample_patterns_builder
635
+ end
636
+
637
+ # Enrich ASN events before conversion
638
+ def enrich(&block)
639
+ @enrich_block = block
640
+ end
641
+
642
+ # Get final event mapping (after overrides)
643
+ def event_mapping
644
+ mapping = E11y::Instruments::RailsInstrumentation::DEFAULT_RAILS_EVENT_MAPPING.dup
645
+ mapping.merge!(@custom_event_classes)
646
+ mapping.except(*@ignored_events)
647
+ end
648
+
649
+ # Check if event should be tracked
650
+ def track?(asn_pattern)
651
+ return false if @ignored_events.include?(asn_pattern)
652
+ return true if @track_patterns.empty?
653
+
654
+ @track_patterns.any? do |pattern|
655
+ File.fnmatch(pattern, asn_pattern)
656
+ end
657
+ end
658
+ end
659
+ end
660
+ end
661
+ ```
662
+
663
+ # ========================================
664
+ # Sidekiq
665
+ # ========================================
666
+ sidekiq do
667
+ enabled true # Set to false to disable Sidekiq integration
668
+
669
+ # Server middleware (job execution)
670
+ server_middleware do
671
+ enabled true
672
+ track_start true
673
+ track_complete true
674
+ track_failure true
675
+ end
676
+
677
+ # Client middleware (job enqueuing)
678
+ client_middleware do
679
+ enabled true
680
+ track_enqueue true
681
+ end
682
+
683
+ # Trace propagation
684
+ propagate_trace_context true
685
+ trace_context_keys ['e11y_trace_id', 'e11y_span_id', 'e11y_sampled']
686
+ end
687
+
688
+ # ========================================
689
+ # ActiveJob
690
+ # ========================================
691
+ active_job do
692
+ enabled true # Set to false to disable ActiveJob integration
693
+
694
+ track_enqueue true
695
+ track_start true
696
+ track_complete true
697
+ track_failure true
698
+
699
+ # Trace propagation
700
+ propagate_trace_context true
701
+
702
+ # Job-scoped buffer (like request-scoped buffer for HTTP)
703
+ use_job_buffer true
704
+
705
+ job_buffer do
706
+ buffer_severities [:debug]
707
+ flush_on do
708
+ error true # Flush debug events if job fails
709
+ success false # Discard debug events if job succeeds
710
+ end
711
+ max_events 1000
712
+ end
713
+ end
714
+
715
+ # ========================================
716
+ # Rack Middleware
717
+ # ========================================
718
+ rack_middleware do
719
+ enabled true # Set to false to disable Rack middleware
720
+
721
+ track_request_start true
722
+ track_request_complete true
723
+ track_request_failure true
724
+
725
+ # Request-scoped buffer
726
+ use_request_buffer true
727
+ end
728
+ end
729
+ end
730
+ ```
731
+
732
+ **Example: Disable specific instruments:**
733
+
734
+ ```ruby
735
+ # Disable ASN but keep Sidekiq
736
+ E11y.configure do |config|
737
+ config.instruments.active_support_notifications.enabled = false
738
+ config.instruments.sidekiq.enabled = true
739
+ config.instruments.active_job.enabled = true
740
+ end
741
+
742
+ # Minimal setup: only Sidekiq
743
+ E11y.configure do |config|
744
+ config.instruments do
745
+ active_support_notifications { enabled false }
746
+ sidekiq { enabled true }
747
+ active_job { enabled false }
748
+ rack_middleware { enabled false }
749
+ end
750
+ end
751
+ ```
752
+
753
+ ---
754
+
755
+ ## 4.3. Built-in Event Classes
756
+
757
+ **Design Decision:** E11y provides built-in event classes for standard Rails events.
758
+
759
+ **Location:** `Events::Rails` namespace (auto-loaded by gem)
760
+
761
+ ```ruby
762
+ # app/events/rails/ (provided by E11y gem)
763
+ module Events
764
+ module Rails
765
+ module Database
766
+ class Query < E11y::Event::Base
767
+ schema do
768
+ required(:name).filled(:string)
769
+ required(:sql).filled(:string)
770
+ required(:duration).filled(:float)
771
+ optional(:binds).array(:hash)
772
+ end
773
+
774
+ severity :debug
775
+ adapters [:stdout, :loki] # Default adapters for SQL queries
776
+ end
777
+ end
778
+
779
+ module Http
780
+ class Request < E11y::Event::Base
781
+ schema do
782
+ required(:controller).filled(:string)
783
+ required(:action).filled(:string)
784
+ required(:method).filled(:string)
785
+ required(:path).filled(:string)
786
+ required(:format).filled(:string)
787
+ required(:status).filled(:integer)
788
+ required(:duration).filled(:float)
789
+ optional(:view_runtime).filled(:float)
790
+ optional(:db_runtime).filled(:float)
791
+ end
792
+
793
+ severity :info
794
+ end
795
+ end
796
+
797
+ module Cache
798
+ class Read < E11y::Event::Base
799
+ schema do
800
+ required(:key).filled(:string)
801
+ required(:hit).filled(:bool)
802
+ optional(:duration).filled(:float)
803
+ end
804
+
805
+ severity :debug
806
+ end
807
+ end
808
+
809
+ module Job
810
+ class Enqueued < E11y::Event::Base
811
+ schema do
812
+ required(:job_class).filled(:string)
813
+ required(:job_id).filled(:string)
814
+ required(:queue).filled(:string)
815
+ optional(:scheduled_at).filled(:time)
816
+ end
817
+
818
+ severity :info
819
+ end
820
+
821
+ class Started < E11y::Event::Base
822
+ schema do
823
+ required(:job_class).filled(:string)
824
+ required(:job_id).filled(:string)
825
+ required(:queue).filled(:string)
826
+ end
827
+
828
+ severity :info
829
+ end
830
+
831
+ class Completed < E11y::Event::Base
832
+ schema do
833
+ required(:job_class).filled(:string)
834
+ required(:job_id).filled(:string)
835
+ required(:duration).filled(:float)
836
+ end
837
+
838
+ severity :success # Extended severity
839
+ end
840
+
841
+ class Failed < E11y::Event::Base
842
+ schema do
843
+ required(:job_class).filled(:string)
844
+ required(:job_id).filled(:string)
845
+ required(:duration).filled(:float)
846
+ required(:error_class).filled(:string)
847
+ required(:error_message).filled(:string)
848
+ end
849
+
850
+ severity :error
851
+ adapters [:loki, :sentry] # Send failures to Sentry
852
+ end
853
+ end
854
+ end
855
+ end
856
+ ```
857
+
858
+ **User can override:**
859
+
860
+ ```ruby
861
+ # config/initializers/e11y.rb
862
+ E11y.configure do |config|
863
+ config.instruments.active_support_notifications do
864
+ # Disable built-in events
865
+ use_built_in_events false
866
+
867
+ # Use custom events instead
868
+ custom_mappings do
869
+ map 'sql.active_record', to: MyApp::Events::DatabaseQuery
870
+ map 'process_action.action_controller', to: MyApp::Events::HttpRequest
871
+ end
872
+ end
873
+ end
874
+ ```
875
+
876
+ ---
877
+
878
+ ## 5. Sidekiq Integration
879
+
880
+ ### 5.1. Server Middleware (Job Execution)
881
+
882
+ ```ruby
883
+ # lib/e11y/instruments/sidekiq/server_middleware.rb
884
+ module E11y
885
+ module Instruments
886
+ module Sidekiq
887
+ class ServerMiddleware
888
+ def call(worker, job, queue)
889
+ # Extract trace context from job metadata
890
+ trace_id = job['e11y_trace_id'] || E11y::TraceContext.generate_id
891
+ parent_span_id = job['e11y_span_id']
892
+
893
+ # Restore trace context
894
+ E11y::Current.set(
895
+ trace_id: trace_id,
896
+ parent_span_id: parent_span_id,
897
+ job_id: job['jid'],
898
+ job_class: worker.class.name,
899
+ queue: queue
900
+ )
901
+
902
+ # Start job-scoped buffer (optional, configurable)
903
+ if E11y.config.instruments.sidekiq.use_job_buffer
904
+ E11y::JobBuffer.start!
905
+ end
906
+
907
+ # Track job start
908
+ Events::Rails::Job::Started.track(
909
+ job_class: worker.class.name,
910
+ job_id: job['jid'],
911
+ queue: queue,
912
+ args: sanitize_args(job['args']),
913
+ enqueued_at: Time.at(job['enqueued_at'])
914
+ )
915
+
916
+ start_time = Time.now
917
+
918
+ begin
919
+ result = yield
920
+
921
+ # Track job success
922
+ Events::Rails::Job::Completed.track(
923
+ job_class: worker.class.name,
924
+ job_id: job['jid'],
925
+ duration: (Time.now - start_time) * 1000,
926
+ queue: queue
927
+ )
928
+
929
+ # Flush job buffer (success case)
930
+ E11y::JobBuffer.flush! if E11y.config.instruments.sidekiq.use_job_buffer
931
+
932
+ result
933
+ rescue => error
934
+ # Track job failure
935
+ Events::Rails::Job::Failed.track(
936
+ job_class: worker.class.name,
937
+ job_id: job['jid'],
938
+ duration: (Time.now - start_time) * 1000,
939
+ queue: queue,
940
+ error_class: error.class.name,
941
+ error_message: error.message,
942
+ backtrace: error.backtrace&.first(10)
943
+ )
944
+
945
+ # Flush job buffer on error (includes debug events)
946
+ E11y::JobBuffer.flush_on_error! if E11y.config.instruments.sidekiq.use_job_buffer
947
+
948
+ raise
949
+ ensure
950
+ E11y::Current.reset
951
+ end
952
+ end
953
+
954
+ private
955
+
956
+ def sanitize_args(args)
957
+ # Limit size and filter PII
958
+ args.first(5).map { |arg| truncate(arg.inspect, 100) }
959
+ end
960
+
961
+ def truncate(string, max_length)
962
+ string.length > max_length ? "#{string[0...max_length]}..." : string
963
+ end
964
+ end
965
+ end
966
+ end
967
+ end
968
+ ```
969
+
970
+ ### 5.2. Client Middleware (Job Enqueuing)
971
+
972
+ ```ruby
973
+ # lib/e11y/instruments/sidekiq/client_middleware.rb
974
+ module E11y
975
+ module Instruments
976
+ module Sidekiq
977
+ class ClientMiddleware
978
+ def call(worker_class, job, queue, redis_pool)
979
+ # Propagate trace context to job
980
+ job['e11y_trace_id'] = E11y::Current.trace_id
981
+ job['e11y_span_id'] = E11y::TraceContext.generate_span_id
982
+ job['e11y_sampled'] = E11y::Current.sampled # Trace-consistent sampling
983
+
984
+ # Track job enqueued
985
+ Events::Rails::Job::Enqueued.track(
986
+ job_class: worker_class.to_s,
987
+ job_id: job['jid'],
988
+ queue: queue,
989
+ scheduled_at: job['at'] ? Time.at(job['at']) : nil
990
+ )
991
+
992
+ yield
993
+ end
994
+ end
995
+ end
996
+ end
997
+ end
998
+ ```
999
+
1000
+ ### 5.3. Job-Scoped Buffer (Optional Feature)
1001
+
1002
+ **Design Decision:** Similar to request-scoped buffer, jobs can have their own buffer for debug events.
1003
+
1004
+ ```ruby
1005
+ # config/initializers/e11y.rb
1006
+ E11y.configure do |config|
1007
+ config.instruments.sidekiq do
1008
+ # Enable job-scoped buffer (like request-scoped buffer)
1009
+ use_job_buffer true
1010
+
1011
+ job_buffer do
1012
+ # Buffer debug events during job execution
1013
+ buffer_severities [:debug]
1014
+
1015
+ # Flush conditions
1016
+ flush_on do
1017
+ error true # Flush debug events if job fails
1018
+ success false # Discard debug events if job succeeds
1019
+ interval 5.seconds # Or flush every 5 seconds
1020
+ end
1021
+
1022
+ # Max buffer size per job
1023
+ max_events 1000
1024
+ end
1025
+ end
1026
+ end
1027
+ ```
1028
+
1029
+ **How it works:**
1030
+
1031
+ ```ruby
1032
+ class InvoiceGenerationWorker
1033
+ include Sidekiq::Worker
1034
+
1035
+ def perform(order_id)
1036
+ # Debug events are buffered
1037
+ Events::Debug::FetchOrder.track(order_id: order_id)
1038
+
1039
+ order = Order.find(order_id)
1040
+
1041
+ Events::Debug::ValidateOrder.track(order_id: order_id, valid: order.valid?)
1042
+
1043
+ if order.invalid?
1044
+ # Job fails → debug events are flushed to adapters
1045
+ raise "Invalid order"
1046
+ end
1047
+
1048
+ # Job succeeds → debug events are discarded
1049
+ Events::InvoiceGenerated.track(order_id: order_id)
1050
+ end
1051
+ end
1052
+ ```
1053
+
1054
+ **Job Buffer Lifecycle:**
1055
+
1056
+ ```mermaid
1057
+ sequenceDiagram
1058
+ participant Worker as Sidekiq Worker
1059
+ participant JobBuffer as Job Buffer
1060
+ participant MainBuffer as Main Buffer
1061
+ participant Adapters as Adapters
1062
+
1063
+ Worker->>JobBuffer: Start job buffer
1064
+
1065
+ Note over Worker: Job execution starts
1066
+
1067
+ Worker->>JobBuffer: Track debug event
1068
+ JobBuffer->>JobBuffer: Buffer (not flushed)
1069
+
1070
+ Worker->>MainBuffer: Track info event
1071
+ MainBuffer->>Adapters: Flush immediately (normal flow)
1072
+
1073
+ alt Job succeeds
1074
+ Worker->>JobBuffer: flush! (success)
1075
+ JobBuffer->>JobBuffer: Discard debug events
1076
+ Note over JobBuffer: Debug events never sent
1077
+ else Job fails
1078
+ Worker->>JobBuffer: flush_on_error!
1079
+ JobBuffer->>MainBuffer: Move debug events to main buffer
1080
+ MainBuffer->>Adapters: Flush all events (including debug)
1081
+ end
1082
+ ```
1083
+
1084
+ ---
1085
+
1086
+ ### 5.4. Trace Propagation Diagram
1087
+
1088
+ ```mermaid
1089
+ sequenceDiagram
1090
+ participant Controller as Rails Controller
1091
+ participant ClientMW as Sidekiq Client MW
1092
+ participant Redis as Redis Queue
1093
+ participant ServerMW as Sidekiq Server MW
1094
+ participant Worker as Worker
1095
+ participant E11y as E11y
1096
+
1097
+ Note over Controller: trace_id: abc123<br/>span_id: span001
1098
+
1099
+ Controller->>ClientMW: enqueue job
1100
+ ClientMW->>ClientMW: Extract trace_id from Current
1101
+ ClientMW->>Redis: Store job + trace metadata
1102
+
1103
+ Note over Redis: job['e11y_trace_id'] = 'abc123'<br/>job['e11y_span_id'] = 'span002'<br/>job['e11y_sampled'] = true
1104
+
1105
+ ClientMW->>E11y: Track Enqueued event
1106
+
1107
+ Note over ServerMW: Later... job dequeued
1108
+
1109
+ Redis->>ServerMW: Fetch job
1110
+ ServerMW->>ServerMW: Extract trace_id from job
1111
+ ServerMW->>E11y: Restore Current context
1112
+
1113
+ Note over E11y: Current.trace_id = 'abc123'<br/>Current.parent_span_id = 'span002'
1114
+
1115
+ ServerMW->>E11y: Track Started event
1116
+ ServerMW->>Worker: perform
1117
+ Worker->>E11y: Track custom events (same trace_id!)
1118
+ ServerMW->>E11y: Track Completed event
1119
+ ```
1120
+
1121
+ ---
1122
+
1123
+ ## 6. ActiveJob Integration
1124
+
1125
+ ### 6.1. Callbacks Integration
1126
+
1127
+ ```ruby
1128
+ # lib/e11y/instruments/active_job/callbacks.rb
1129
+ module E11y
1130
+ module Instruments
1131
+ module ActiveJob
1132
+ module Callbacks
1133
+ extend ActiveSupport::Concern
1134
+
1135
+ included do
1136
+ around_perform :e11y_track_job_execution
1137
+ after_enqueue :e11y_track_job_enqueued
1138
+ end
1139
+
1140
+ private
1141
+
1142
+ def e11y_track_job_execution
1143
+ # Extract trace context
1144
+ trace_id = job_metadata['e11y_trace_id'] || E11y::TraceContext.generate_id
1145
+ parent_span_id = job_metadata['e11y_span_id']
1146
+
1147
+ E11y::Current.set(
1148
+ trace_id: trace_id,
1149
+ parent_span_id: parent_span_id,
1150
+ job_id: job_id,
1151
+ job_class: self.class.name
1152
+ )
1153
+
1154
+ # Start job-scoped buffer (optional, configurable)
1155
+ if E11y.config.instruments.active_job.use_job_buffer
1156
+ E11y::JobBuffer.start!
1157
+ end
1158
+
1159
+ Events::Rails::Job::Started.track(
1160
+ job_class: self.class.name,
1161
+ job_id: job_id,
1162
+ queue_name: queue_name,
1163
+ arguments: sanitized_arguments
1164
+ )
1165
+
1166
+ start_time = Time.now
1167
+
1168
+ begin
1169
+ yield
1170
+
1171
+ Events::Rails::Job::Completed.track(
1172
+ job_class: self.class.name,
1173
+ job_id: job_id,
1174
+ duration: (Time.now - start_time) * 1000
1175
+ )
1176
+
1177
+ # Flush job buffer (success case)
1178
+ E11y::JobBuffer.flush! if E11y.config.instruments.active_job.use_job_buffer
1179
+ rescue => error
1180
+ Events::Rails::Job::Failed.track(
1181
+ job_class: self.class.name,
1182
+ job_id: job_id,
1183
+ duration: (Time.now - start_time) * 1000,
1184
+ error_class: error.class.name,
1185
+ error_message: error.message
1186
+ )
1187
+
1188
+ # Flush job buffer on error (includes debug events)
1189
+ E11y::JobBuffer.flush_on_error! if E11y.config.instruments.active_job.use_job_buffer
1190
+
1191
+ raise
1192
+ ensure
1193
+ E11y::Current.reset
1194
+ end
1195
+ end
1196
+
1197
+ def e11y_track_job_enqueued
1198
+ # Store trace context in job metadata
1199
+ job_metadata['e11y_trace_id'] = E11y::Current.trace_id
1200
+ job_metadata['e11y_span_id'] = E11y::TraceContext.generate_span_id
1201
+ job_metadata['e11y_sampled'] = E11y::Current.sampled
1202
+
1203
+ Events::Rails::Job::Enqueued.track(
1204
+ job_class: self.class.name,
1205
+ job_id: job_id,
1206
+ queue_name: queue_name,
1207
+ scheduled_at: scheduled_at
1208
+ )
1209
+ end
1210
+
1211
+ def job_metadata
1212
+ @e11y_metadata ||= (provider_job_id || {})
1213
+ end
1214
+
1215
+ def sanitized_arguments
1216
+ arguments.map { |arg| E11y::Sanitizer.sanitize(arg) }
1217
+ end
1218
+ end
1219
+ end
1220
+ end
1221
+ end
1222
+ ```
1223
+
1224
+ ---
1225
+
1226
+ ## 7. Rails.logger Migration
1227
+
1228
+ ### 7.1. Logger Bridge
1229
+
1230
+ **Design Decision:** Drop-in replacement for Rails.logger.
1231
+
1232
+ ```ruby
1233
+ # lib/e11y/logger/bridge.rb
1234
+ module E11y
1235
+ module Logger
1236
+ class Bridge
1237
+ def self.setup!
1238
+ return unless E11y.config.logger_bridge.enabled
1239
+
1240
+ # Replace Rails.logger
1241
+ Rails.logger = Bridge.new(Rails.logger)
1242
+ end
1243
+
1244
+ def initialize(original_logger = nil)
1245
+ @original_logger = original_logger
1246
+ @severity_mapping = {
1247
+ Logger::DEBUG => :debug,
1248
+ Logger::INFO => :info,
1249
+ Logger::WARN => :warn,
1250
+ Logger::ERROR => :error,
1251
+ Logger::FATAL => :fatal
1252
+ }
1253
+ end
1254
+
1255
+ # Standard logger methods
1256
+ def debug(message = nil, &block)
1257
+ log(:debug, message, &block)
1258
+ end
1259
+
1260
+ def info(message = nil, &block)
1261
+ log(:info, message, &block)
1262
+ end
1263
+
1264
+ def warn(message = nil, &block)
1265
+ log(:warn, message, &block)
1266
+ end
1267
+
1268
+ def error(message = nil, &block)
1269
+ log(:error, message, &block)
1270
+ end
1271
+
1272
+ def fatal(message = nil, &block)
1273
+ log(:fatal, message, &block)
1274
+ end
1275
+
1276
+ # Generic log method
1277
+ def add(severity, message = nil, progname = nil, &block)
1278
+ e11y_severity = @severity_mapping[severity] || :info
1279
+ log(e11y_severity, message || progname, &block)
1280
+ end
1281
+
1282
+ alias_method :log, :add
1283
+
1284
+ # Compatibility methods
1285
+ def level
1286
+ @original_logger&.level || Logger::DEBUG
1287
+ end
1288
+
1289
+ def level=(new_level)
1290
+ @original_logger&.level = new_level if @original_logger
1291
+ end
1292
+
1293
+ def formatter
1294
+ @original_logger&.formatter
1295
+ end
1296
+
1297
+ def formatter=(new_formatter)
1298
+ @original_logger.formatter = new_formatter if @original_logger
1299
+ end
1300
+
1301
+ private
1302
+
1303
+ def log(severity, message = nil, &block)
1304
+ # Extract message
1305
+ msg = message || (block_given? ? block.call : nil)
1306
+
1307
+ # Track via E11y
1308
+ Events::RailsLogger.track(
1309
+ severity: severity,
1310
+ message: msg.to_s,
1311
+ caller_location: extract_caller_location
1312
+ )
1313
+
1314
+ # Also log to original logger (dual logging)
1315
+ if @original_logger && E11y.config.logger_bridge.dual_logging
1316
+ @original_logger.public_send(severity, msg)
1317
+ end
1318
+ end
1319
+
1320
+ def extract_caller_location
1321
+ # Find first caller outside E11y
1322
+ caller_locations.find { |loc|
1323
+ !loc.path.include?('e11y')
1324
+ }&.then { |loc|
1325
+ "#{loc.path}:#{loc.lineno}:in `#{loc.label}'"
1326
+ }
1327
+ end
1328
+ end
1329
+ end
1330
+ end
1331
+ ```
1332
+
1333
+ ### 7.2. Migration Strategy
1334
+
1335
+ ```ruby
1336
+ # config/initializers/e11y.rb
1337
+ E11y.configure do |config|
1338
+ config.logger_bridge do
1339
+ enabled true
1340
+
1341
+ # Dual logging (E11y + original Rails.logger)
1342
+ dual_logging true # Keep writing to log/production.log
1343
+
1344
+ # Which severities to track
1345
+ track_severities [:info, :warn, :error, :fatal]
1346
+
1347
+ # Skip noisy log messages
1348
+ ignore_patterns [
1349
+ /Started GET/,
1350
+ /Completed \d+ OK/,
1351
+ /CACHE/
1352
+ ]
1353
+
1354
+ # Sample high-volume logs
1355
+ sample_rate 0.1 # 10% of logs
1356
+
1357
+ # Enrich with Rails context
1358
+ enrich_with_context true
1359
+ context_fields [:controller, :action, :request_id]
1360
+ end
1361
+ end
1362
+ ```
1363
+
1364
+ ---
1365
+
1366
+ ## 8. Middleware Integration
1367
+
1368
+ ### 8.0. Three Buffer Types (Summary)
1369
+
1370
+ **E11y has 3 independent buffer types:**
1371
+
1372
+ | Buffer Type | Purpose | Lifecycle | Use Case |
1373
+ |-------------|---------|-----------|----------|
1374
+ | **Main Buffer** | All events (info+) | Global, flush every 200ms | Normal event tracking |
1375
+ | **Request Buffer** | Debug events in HTTP requests | Per-request, flush on error | HTTP request debugging |
1376
+ | **Job Buffer** | Debug events in background jobs | Per-job, flush on error | Background job debugging |
1377
+
1378
+ **Diagram:**
1379
+
1380
+ ```mermaid
1381
+ graph TB
1382
+ subgraph "HTTP Request"
1383
+ HTTPEvent[Event tracked] --> Decision1{Severity?}
1384
+ Decision1 -->|:debug| RequestBuffer[Request Buffer]
1385
+ Decision1 -->|:info+| MainBuffer[Main Buffer]
1386
+
1387
+ RequestBuffer --> OnError1{Request failed?}
1388
+ OnError1 -->|Yes| MainBuffer
1389
+ OnError1 -->|No| Discard1[Discard]
1390
+ end
1391
+
1392
+ subgraph "Background Job"
1393
+ JobEvent[Event tracked] --> Decision2{Severity?}
1394
+ Decision2 -->|:debug| JobBuffer[Job Buffer]
1395
+ Decision2 -->|:info+| MainBuffer2[Main Buffer]
1396
+
1397
+ JobBuffer --> OnError2{Job failed?}
1398
+ OnError2 -->|Yes| MainBuffer2
1399
+ OnError2 -->|No| Discard2[Discard]
1400
+ end
1401
+
1402
+ subgraph "Global"
1403
+ MainBuffer --> Interval[Every 200ms]
1404
+ MainBuffer2 --> Interval
1405
+ Interval --> Adapters[Flush to Adapters]
1406
+ end
1407
+
1408
+ style RequestBuffer fill:#fff3cd
1409
+ style JobBuffer fill:#d4edda
1410
+ style MainBuffer fill:#d1ecf1
1411
+ style MainBuffer2 fill:#d1ecf1
1412
+ ```
1413
+
1414
+ **Configuration:**
1415
+
1416
+ ```ruby
1417
+ E11y.configure do |config|
1418
+ # Main buffer (always enabled)
1419
+ config.buffer.flush_interval = 200.milliseconds
1420
+ config.buffer.max_size = 10_000
1421
+
1422
+ # Request-scoped buffer (HTTP only)
1423
+ config.instruments.rack_middleware.use_request_buffer = true
1424
+
1425
+ # Job-scoped buffer (Sidekiq + ActiveJob)
1426
+ config.instruments.sidekiq.use_job_buffer = true
1427
+ config.instruments.active_job.use_job_buffer = true
1428
+ end
1429
+ ```
1430
+
1431
+ ---
1432
+
1433
+ ### 8.1. Request Middleware
1434
+
1435
+ ```ruby
1436
+ # lib/e11y/middleware/request.rb
1437
+ module E11y
1438
+ module Middleware
1439
+ class Request
1440
+ def initialize(app)
1441
+ @app = app
1442
+ end
1443
+
1444
+ def call(env)
1445
+ request = Rack::Request.new(env)
1446
+
1447
+ # Extract or generate trace_id
1448
+ trace_id = extract_trace_id(request) || TraceContext.generate_id
1449
+ span_id = TraceContext.generate_span_id
1450
+
1451
+ # Set context
1452
+ Current.set(
1453
+ trace_id: trace_id,
1454
+ span_id: span_id,
1455
+ request_id: request_id(env),
1456
+ user_id: extract_user_id(env),
1457
+ ip_address: request.ip,
1458
+ user_agent: request.user_agent
1459
+ )
1460
+
1461
+ # Start request-scoped buffer (for debug events)
1462
+ # Note: This is ONLY for HTTP requests, not for jobs
1463
+ # Jobs have their own JobBuffer (see Sidekiq/ActiveJob sections)
1464
+ if E11y.config.instruments.rack_middleware.use_request_buffer
1465
+ E11y::RequestBuffer.start!
1466
+ end
1467
+
1468
+ # Track request start
1469
+ start_time = Time.now
1470
+
1471
+ Events::Http::RequestStarted.track(
1472
+ method: request.request_method,
1473
+ path: request.path,
1474
+ query: request.query_string,
1475
+ format: request.format
1476
+ )
1477
+
1478
+ begin
1479
+ status, headers, body = @app.call(env)
1480
+
1481
+ # Track request completed
1482
+ Events::Http::RequestCompleted.track(
1483
+ method: request.request_method,
1484
+ path: request.path,
1485
+ status: status,
1486
+ duration: (Time.now - start_time) * 1000
1487
+ )
1488
+
1489
+ # Add trace headers to response
1490
+ headers['X-E11y-Trace-Id'] = trace_id
1491
+ headers['X-E11y-Span-Id'] = span_id
1492
+
1493
+ [status, headers, body]
1494
+ rescue => error
1495
+ # Track request failed
1496
+ Events::Http::RequestFailed.track(
1497
+ method: request.request_method,
1498
+ path: request.path,
1499
+ duration: (Time.now - start_time) * 1000,
1500
+ error_class: error.class.name,
1501
+ error_message: error.message
1502
+ )
1503
+
1504
+ # Flush request buffer (includes debug events on error)
1505
+ if E11y.config.instruments.rack_middleware.use_request_buffer
1506
+ E11y::RequestBuffer.flush_on_error!
1507
+ end
1508
+
1509
+ raise
1510
+ ensure
1511
+ # Flush request buffer (success case)
1512
+ if E11y.config.instruments.rack_middleware.use_request_buffer && !error
1513
+ E11y::RequestBuffer.flush!
1514
+ end
1515
+
1516
+ # Reset context
1517
+ Current.reset
1518
+ end
1519
+ end
1520
+
1521
+ private
1522
+
1523
+ def extract_trace_id(request)
1524
+ # W3C Trace Context
1525
+ request.get_header('HTTP_TRACEPARENT')&.split('-')&.[](1) ||
1526
+ # X-Request-ID
1527
+ request.get_header('HTTP_X_REQUEST_ID') ||
1528
+ # X-Trace-Id
1529
+ request.get_header('HTTP_X_TRACE_ID')
1530
+ end
1531
+
1532
+ def request_id(env)
1533
+ env['action_dispatch.request_id'] || SecureRandom.uuid
1534
+ end
1535
+
1536
+ def extract_user_id(env)
1537
+ # Try to extract from Warden (Devise)
1538
+ env['warden']&.user&.id ||
1539
+ # Try to extract from session
1540
+ env['rack.session']&.[]('user_id')
1541
+ end
1542
+ end
1543
+ end
1544
+ end
1545
+ ```
1546
+
1547
+ ---
1548
+
1549
+ ## 9. Console & Development
1550
+
1551
+ ### 9.1. Console Helpers
1552
+
1553
+ ```ruby
1554
+ # lib/e11y/console.rb
1555
+ module E11y
1556
+ module Console
1557
+ def self.enable!
1558
+ define_helper_methods
1559
+ configure_for_console
1560
+ end
1561
+
1562
+ def self.define_helper_methods
1563
+ # E11y.stats
1564
+ def E11y.stats
1565
+ {
1566
+ events_tracked: Registry.all_events.sum { |e| e.track_count },
1567
+ events_in_buffer: Buffer.size,
1568
+ adapters: Adapters::Registry.all.map { |a|
1569
+ { name: a.name, healthy: a.healthy? }
1570
+ },
1571
+ rate_limiter: {
1572
+ current_rate: RateLimiter.current_rate,
1573
+ limit: RateLimiter.limit
1574
+ }
1575
+ }
1576
+ end
1577
+
1578
+ # E11y.test_event
1579
+ def E11y.test_event
1580
+ Events::Console::Test.track(
1581
+ message: 'Test event from console',
1582
+ timestamp: Time.now
1583
+ )
1584
+
1585
+ puts "✅ Test event tracked!"
1586
+ puts "Check adapters: E11y.stats"
1587
+ end
1588
+
1589
+ # E11y.events
1590
+ def E11y.events
1591
+ Registry.all_events.map(&:name).sort
1592
+ end
1593
+
1594
+ # E11y.adapters
1595
+ def E11y.adapters
1596
+ Adapters::Registry.all.map do |adapter|
1597
+ {
1598
+ name: adapter.name,
1599
+ class: adapter.class.name,
1600
+ healthy: adapter.healthy?,
1601
+ capabilities: adapter.capabilities
1602
+ }
1603
+ end
1604
+ end
1605
+
1606
+ # E11y.reset!
1607
+ def E11y.reset!
1608
+ Buffer.clear!
1609
+ RequestBuffer.clear!
1610
+ puts "✅ Buffers cleared"
1611
+ end
1612
+ end
1613
+
1614
+ def self.configure_for_console
1615
+ E11y.configure do |config|
1616
+ # Console-friendly output
1617
+ config.adapters.clear
1618
+ config.adapters.register :stdout, Adapters::Stdout.new(
1619
+ colorize: true,
1620
+ pretty_print: true
1621
+ )
1622
+
1623
+ # Disable rate limiting in console
1624
+ config.rate_limiting.enabled = false
1625
+
1626
+ # Show all severities
1627
+ config.severity_threshold = :debug
1628
+ end
1629
+ end
1630
+ end
1631
+ end
1632
+ ```
1633
+
1634
+ ### 9.2. Development Web UI
1635
+
1636
+ ```ruby
1637
+ # lib/e11y/web_ui.rb
1638
+ module E11y
1639
+ class WebUI
1640
+ def self.mount!(app)
1641
+ app.mount E11y::WebUI::Engine, at: '/e11y'
1642
+ end
1643
+ end
1644
+
1645
+ module WebUI
1646
+ class Engine < Rails::Engine
1647
+ isolate_namespace E11y::WebUI
1648
+
1649
+ # Routes
1650
+ initializer 'e11y_web_ui.routes' do
1651
+ E11y::WebUI::Engine.routes.draw do
1652
+ root to: 'dashboard#index'
1653
+
1654
+ resources :events, only: [:index, :show]
1655
+ resources :adapters, only: [:index, :show]
1656
+
1657
+ get '/stats', to: 'stats#index'
1658
+ get '/registry', to: 'registry#index'
1659
+ end
1660
+ end
1661
+ end
1662
+ end
1663
+ end
1664
+ ```
1665
+
1666
+ ---
1667
+
1668
+ ## 10. Testing in Rails
1669
+
1670
+ ### 10.1. RSpec Integration
1671
+
1672
+ ```ruby
1673
+ # lib/e11y/testing/rspec.rb
1674
+ module E11y
1675
+ module Testing
1676
+ module RSpec
1677
+ def self.setup!
1678
+ ::RSpec.configure do |config|
1679
+ # Use in-memory adapter for tests
1680
+ config.before(:suite) do
1681
+ E11y.configure do |e11y_config|
1682
+ e11y_config.adapters.clear
1683
+ e11y_config.adapters.register :test, E11y::Adapters::InMemory.new
1684
+ end
1685
+ end
1686
+
1687
+ # Clear events between tests
1688
+ config.after(:each) do
1689
+ E11y.test_adapter.clear!
1690
+ end
1691
+
1692
+ # Include helpers
1693
+ config.include E11y::Testing::Matchers
1694
+ end
1695
+ end
1696
+ end
1697
+
1698
+ module Matchers
1699
+ # have_tracked_event matcher
1700
+ def have_tracked_event(event_class_or_name)
1701
+ HaveTrackedEventMatcher.new(event_class_or_name)
1702
+ end
1703
+
1704
+ class HaveTrackedEventMatcher
1705
+ def initialize(event_class_or_name)
1706
+ @event_class_or_name = event_class_or_name
1707
+ @payload_matchers = {}
1708
+ end
1709
+
1710
+ def with(payload)
1711
+ @payload_matchers = payload
1712
+ self
1713
+ end
1714
+
1715
+ def matches?(actual = nil)
1716
+ events = E11y.test_adapter.find_events(event_pattern)
1717
+
1718
+ return false if events.empty?
1719
+
1720
+ if @payload_matchers.any?
1721
+ events.any? { |event| payload_matches?(event) }
1722
+ else
1723
+ true
1724
+ end
1725
+ end
1726
+
1727
+ def failure_message
1728
+ if E11y.test_adapter.events.empty?
1729
+ "expected #{@event_class_or_name} to be tracked, but no events were tracked"
1730
+ else
1731
+ tracked = E11y.test_adapter.events.map { |e| e[:event_name] }.join(', ')
1732
+ "expected #{@event_class_or_name} to be tracked, but only tracked: #{tracked}"
1733
+ end
1734
+ end
1735
+
1736
+ private
1737
+
1738
+ def event_pattern
1739
+ if @event_class_or_name.is_a?(Class)
1740
+ @event_class_or_name.event_name
1741
+ else
1742
+ @event_class_or_name
1743
+ end
1744
+ end
1745
+
1746
+ def payload_matches?(event)
1747
+ @payload_matchers.all? do |key, expected|
1748
+ event[:payload][key] == expected
1749
+ end
1750
+ end
1751
+ end
1752
+ end
1753
+ end
1754
+ end
1755
+ ```
1756
+
1757
+ ### 10.2. Test Examples
1758
+
1759
+ ```ruby
1760
+ RSpec.describe OrdersController, type: :controller do
1761
+ describe 'POST #create' do
1762
+ it 'tracks order creation event' do
1763
+ post :create, params: { order: { item: 'Book', price: 29.99 } }
1764
+
1765
+ expect(response).to have_tracked_event(Events::OrderCreated)
1766
+ .with(item: 'Book', price: 29.99)
1767
+ end
1768
+
1769
+ it 'propagates trace_id to background job' do
1770
+ expect {
1771
+ post :create, params: { order: { item: 'Book' } }
1772
+ }.to have_enqueued_job(SendOrderEmailJob)
1773
+
1774
+ job = ActiveJob::Base.queue_adapter.enqueued_jobs.last
1775
+ expect(job[:args].first['e11y_trace_id']).to be_present
1776
+ end
1777
+ end
1778
+ end
1779
+ ```
1780
+
1781
+ ---
1782
+
1783
+ ## 11. Trade-offs
1784
+
1785
+ ### 11.1. Key Decisions
1786
+
1787
+ | Decision | Pro | Con | Rationale |
1788
+ |----------|-----|-----|-----------|
1789
+ | **Railtie auto-setup** | Zero config | Less control | DX > control |
1790
+ | **Granular enable/disable** | Flexibility | More config | Production needs |
1791
+ | **Built-in event classes** | Ready to use | Opinionated | Common Rails patterns |
1792
+ | **ASN bidirectional** | Rich integration | Overhead | Leverage Rails |
1793
+ | **Job-scoped buffer** | Debug on error | Memory overhead | Same as request buffer |
1794
+ | **Dual logging** | Gradual migration | Duplication | Safety net |
1795
+ | **In-memory test adapter** | Fast tests | Different from prod | Speed matters |
1796
+ | **Web UI in gem** | Convenient | Gem bloat | Dev experience |
1797
+
1798
+ ### 11.2. Alternatives Considered
1799
+
1800
+ **A) Manual initialization (no Railtie)**
1801
+ - ❌ Rejected: Poor DX, error-prone
1802
+
1803
+ **B) Subscribe to ALL ASN events**
1804
+ - ❌ Rejected: Too much noise, performance impact
1805
+
1806
+ **C) Replace Rails.logger completely**
1807
+ - ❌ Rejected: Breaking change, risky migration
1808
+
1809
+ **D) Separate gem for Rails integration**
1810
+ - ❌ Rejected: Complexity, most users are Rails
1811
+
1812
+ ---
1813
+
1814
+ ## 12. FAQ
1815
+
1816
+ ### Q1: Can I disable specific Rails integrations?
1817
+
1818
+ **Yes!** Each instrument can be enabled/disabled independently:
1819
+
1820
+ ```ruby
1821
+ E11y.configure do |config|
1822
+ config.instruments.active_support_notifications.enabled = false # Disable ASN
1823
+ config.instruments.sidekiq.enabled = true # Keep Sidekiq
1824
+ config.instruments.active_job.enabled = true # Keep ActiveJob
1825
+ config.instruments.rack_middleware.enabled = true # Keep HTTP tracking
1826
+ end
1827
+ ```
1828
+
1829
+ ### Q2: Do you provide built-in event classes for Rails?
1830
+
1831
+ **Yes!** E11y includes `Events::Rails` namespace with common Rails events:
1832
+
1833
+ - `Events::Rails::Database::Query` (sql.active_record)
1834
+ - `Events::Rails::Http::Request` (process_action.action_controller)
1835
+ - `Events::Rails::Cache::Read/Write/Delete`
1836
+ - `Events::Rails::Job::Enqueued/Started/Completed/Failed`
1837
+
1838
+ **You can:**
1839
+ - Use them as-is (default)
1840
+ - Override them with `custom_mappings`
1841
+ - Disable them with `use_built_in_events false`
1842
+
1843
+ ### Q3: Is ActiveSupport::Notifications integration always on?
1844
+
1845
+ **No!** It's configurable:
1846
+
1847
+ ```ruby
1848
+ config.instruments.active_support_notifications do
1849
+ enabled false # Completely disable ASN integration
1850
+ end
1851
+ ```
1852
+
1853
+ You can also filter which ASN events to track:
1854
+
1855
+ ```ruby
1856
+ config.instruments.active_support_notifications do
1857
+ track_patterns ['sql.active_record', 'process_action.*']
1858
+ ignore_patterns ['render_partial.*', 'SCHEMA']
1859
+ end
1860
+ ```
1861
+
1862
+ ### Q4: Does request-scoped buffer work for Sidekiq/ActiveJob?
1863
+
1864
+ **No, they have their own job-scoped buffer!**
1865
+
1866
+ - **Request Buffer** → HTTP requests only (Rack middleware)
1867
+ - **Job Buffer** → Sidekiq + ActiveJob (separate buffer per job)
1868
+ - **Main Buffer** → Global buffer for all info+ events
1869
+
1870
+ All 3 buffers are independent and configurable:
1871
+
1872
+ ```ruby
1873
+ config.instruments.rack_middleware.use_request_buffer = true # HTTP
1874
+ config.instruments.sidekiq.use_job_buffer = true # Sidekiq
1875
+ config.instruments.active_job.use_job_buffer = true # ActiveJob
1876
+ ```
1877
+
1878
+ ### Q5: How do I customize built-in Rails events?
1879
+
1880
+ **Option A: Override with custom event class:**
1881
+
1882
+ ```ruby
1883
+ config.instruments.active_support_notifications do
1884
+ custom_mappings do
1885
+ map 'sql.active_record', to: MyApp::Events::CustomDatabaseQuery
1886
+ end
1887
+ end
1888
+ ```
1889
+
1890
+ **Option B: Disable built-in events entirely:**
1891
+
1892
+ ```ruby
1893
+ config.instruments.active_support_notifications do
1894
+ use_built_in_events false # No automatic mapping
1895
+
1896
+ # Manually handle ASN events
1897
+ enrich do |asn_event|
1898
+ MyCustomHandler.call(asn_event)
1899
+ end
1900
+ end
1901
+ ```
1902
+
1903
+ ### Q6: Can I use E11y without Rails?
1904
+
1905
+ **No.** E11y requires Rails 8.0+ and Ruby 3.3+. For non-Rails apps, consider other telemetry solutions.
1906
+
1907
+ ---
1908
+
1909
+ **Status:** ✅ Draft Complete
1910
+ **Next:** ADR-011 (Testing Strategy) or ADR-013 (Reliability & Error Handling)
1911
+ **Estimated Implementation:** 2 weeks