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