e11y 0.2.0 → 1.0.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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +130 -10
  3. data/CHANGELOG.md +56 -1
  4. data/CLAUDE.md +168 -0
  5. data/CONTRIBUTING.md +640 -0
  6. data/README.md +134 -702
  7. data/RELEASE.md +18 -3
  8. data/Rakefile +108 -29
  9. data/config/README.md +1 -1
  10. data/config/loki-local-config.yaml +12 -0
  11. data/config/otel-collector-config.yaml +44 -0
  12. data/cucumber.yml +1 -0
  13. data/docker-compose.yml +18 -2
  14. data/docs/ADAPTERS.md +76 -0
  15. data/docs/ADAPTIVE_SAMPLING.md +59 -0
  16. data/docs/COMPARISON.md +104 -0
  17. data/docs/CONFIGURATION.md +52 -0
  18. data/docs/DISTRIBUTED_TRACING.md +44 -0
  19. data/docs/LIMITATIONS.md +13 -0
  20. data/docs/METRICS_DSL.md +84 -0
  21. data/docs/PERFORMANCE.md +60 -0
  22. data/docs/PII_FILTERING.md +40 -0
  23. data/docs/PRESETS.md +65 -0
  24. data/docs/QUICK-START.md +546 -587
  25. data/docs/RAILS_INTEGRATION.md +29 -0
  26. data/docs/SCHEMA_VALIDATION.md +63 -0
  27. data/docs/SLO-PROMQL-ALERTS.md +161 -0
  28. data/docs/TESTING.md +69 -0
  29. data/docs/{ADR-001-architecture.md → architecture/ADR-001-architecture.md} +35 -64
  30. data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
  31. data/docs/{ADR-003-slo-observability.md → architecture/ADR-003-slo-observability.md} +27 -466
  32. data/docs/{ADR-004-adapter-architecture.md → architecture/ADR-004-adapter-architecture.md} +163 -146
  33. data/docs/{ADR-005-tracing-context.md → architecture/ADR-005-tracing-context.md} +10 -9
  34. data/docs/{ADR-006-security-compliance.md → architecture/ADR-006-security-compliance.md} +184 -191
  35. data/docs/{ADR-007-opentelemetry-integration.md → architecture/ADR-007-opentelemetry-integration.md} +3 -21
  36. data/docs/{ADR-008-rails-integration.md → architecture/ADR-008-rails-integration.md} +209 -339
  37. data/docs/{ADR-009-cost-optimization.md → architecture/ADR-009-cost-optimization.md} +45 -54
  38. data/docs/architecture/ADR-010-developer-experience.md +522 -0
  39. data/docs/{ADR-011-testing-strategy.md → architecture/ADR-011-testing-strategy.md} +41 -83
  40. data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
  41. data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
  42. data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +23 -41
  43. data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +52 -349
  44. data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
  45. data/docs/architecture/ADR-018-memory-optimization.md +366 -0
  46. data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
  47. data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
  48. data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
  49. data/docs/prd/01-overview-vision.md +19 -14
  50. data/docs/use_cases/README.md +22 -23
  51. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
  52. data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
  53. data/docs/use_cases/UC-003-event-metrics.md +66 -0
  54. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +42 -101
  55. data/docs/use_cases/UC-005-sentry-integration.md +13 -15
  56. data/docs/use_cases/UC-006-trace-context-management.md +30 -28
  57. data/docs/use_cases/UC-007-pii-filtering.md +35 -87
  58. data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
  59. data/docs/use_cases/UC-009-multi-service-tracing.md +4 -4
  60. data/docs/use_cases/UC-010-background-job-tracking.md +5 -5
  61. data/docs/use_cases/UC-011-rate-limiting.md +95 -168
  62. data/docs/use_cases/UC-012-audit-trail.md +21 -46
  63. data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
  64. data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
  65. data/docs/use_cases/UC-015-cost-optimization.md +46 -99
  66. data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
  67. data/docs/use_cases/UC-017-local-development.md +203 -777
  68. data/docs/use_cases/UC-018-testing-events.md +3 -3
  69. data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
  70. data/docs/use_cases/UC-020-event-versioning.md +8 -9
  71. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
  72. data/docs/use_cases/UC-022-event-registry.md +15 -21
  73. data/docs/use_cases/backlog.md +119 -87
  74. data/e11y.gemspec +2 -2
  75. data/gems/e11y-devtools/README.md +136 -0
  76. data/gems/e11y-devtools/config/routes.rb +8 -0
  77. data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
  78. data/gems/e11y-devtools/exe/e11y +34 -0
  79. data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
  80. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
  81. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
  82. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
  83. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
  84. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
  85. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
  86. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
  87. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
  88. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
  89. data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +115 -0
  90. data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +54 -0
  91. data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
  92. data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
  93. data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
  94. data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
  95. data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
  96. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
  97. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
  98. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
  99. data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
  100. data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
  101. data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
  102. data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +58 -0
  103. data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
  104. data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
  105. data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
  106. data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
  107. data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
  108. data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
  109. data/lib/e11y/adapters/audit_encrypted.rb +53 -11
  110. data/lib/e11y/adapters/base.rb +33 -34
  111. data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
  112. data/lib/e11y/adapters/dev_log/query.rb +219 -0
  113. data/lib/e11y/adapters/dev_log.rb +118 -0
  114. data/lib/e11y/adapters/file.rb +3 -6
  115. data/lib/e11y/adapters/in_memory.rb +52 -5
  116. data/lib/e11y/adapters/in_memory_test.rb +29 -0
  117. data/lib/e11y/adapters/loki.rb +58 -23
  118. data/lib/e11y/adapters/null.rb +82 -0
  119. data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
  120. data/lib/e11y/adapters/otel_logs.rb +136 -23
  121. data/lib/e11y/adapters/sentry.rb +4 -7
  122. data/lib/e11y/adapters/stdout.rb +73 -7
  123. data/lib/e11y/adapters/yabeda.rb +153 -29
  124. data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
  125. data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
  126. data/lib/e11y/buffers/ring_buffer.rb +3 -16
  127. data/lib/e11y/configuration.rb +272 -0
  128. data/lib/e11y/console.rb +10 -17
  129. data/lib/e11y/current.rb +53 -1
  130. data/lib/e11y/debug/pipeline_inspector.rb +96 -0
  131. data/lib/e11y/documentation/generator.rb +48 -0
  132. data/lib/e11y/event/base.rb +176 -82
  133. data/lib/e11y/event/value_sampling_config.rb +1 -5
  134. data/lib/e11y/events/rails/database/query.rb +1 -4
  135. data/lib/e11y/events/rails/job/failed.rb +2 -0
  136. data/lib/e11y/instruments/active_job.rb +46 -12
  137. data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
  138. data/lib/e11y/instruments/sidekiq.rb +137 -31
  139. data/lib/e11y/linters/base.rb +11 -0
  140. data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
  141. data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
  142. data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
  143. data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
  144. data/lib/e11y/logger/bridge.rb +26 -7
  145. data/lib/e11y/metrics/cardinality_protection.rb +10 -15
  146. data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
  147. data/lib/e11y/metrics/registry.rb +3 -5
  148. data/lib/e11y/metrics/test_backend.rb +62 -0
  149. data/lib/e11y/metrics.rb +56 -10
  150. data/lib/e11y/middleware/adapter_resolver.rb +40 -0
  151. data/lib/e11y/middleware/audit_signing.rb +43 -6
  152. data/lib/e11y/middleware/baggage_protection.rb +75 -0
  153. data/lib/e11y/middleware/dev_log_source.rb +24 -0
  154. data/lib/e11y/middleware/event_slo.rb +23 -9
  155. data/lib/e11y/middleware/otel_span.rb +23 -0
  156. data/lib/e11y/middleware/pii_filter.rb +104 -75
  157. data/lib/e11y/middleware/rate_limiting.rb +54 -27
  158. data/lib/e11y/middleware/request.rb +70 -23
  159. data/lib/e11y/middleware/routing.rb +78 -21
  160. data/lib/e11y/middleware/sampling.rb +66 -17
  161. data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
  162. data/lib/e11y/middleware/trace_context.rb +45 -10
  163. data/lib/e11y/middleware/track_latency.rb +34 -0
  164. data/lib/e11y/middleware/validation.rb +7 -16
  165. data/lib/e11y/middleware/versioning.rb +26 -22
  166. data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
  167. data/lib/e11y/opentelemetry/span_creator.rb +142 -0
  168. data/lib/e11y/pii/patterns.rb +12 -1
  169. data/lib/e11y/pipeline/builder.rb +1 -1
  170. data/lib/e11y/presets/audit_event.rb +13 -2
  171. data/lib/e11y/railtie.rb +52 -15
  172. data/lib/e11y/registry.rb +306 -0
  173. data/lib/e11y/reliability/circuit_breaker.rb +19 -21
  174. data/lib/e11y/reliability/dlq/base.rb +71 -0
  175. data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
  176. data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
  177. data/lib/e11y/reliability/dlq/filter.rb +37 -54
  178. data/lib/e11y/reliability/retry_handler.rb +26 -29
  179. data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
  180. data/lib/e11y/sampling/error_spike_detector.rb +0 -2
  181. data/lib/e11y/sampling/load_monitor.rb +5 -9
  182. data/lib/e11y/sampling/stratified_tracker.rb +18 -0
  183. data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
  184. data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
  185. data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
  186. data/lib/e11y/slo/config_loader.rb +40 -0
  187. data/lib/e11y/slo/config_validator.rb +58 -0
  188. data/lib/e11y/slo/dashboard_generator.rb +122 -0
  189. data/lib/e11y/slo/event_driven.rb +8 -0
  190. data/lib/e11y/slo/tracker.rb +31 -4
  191. data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
  192. data/lib/e11y/testing/rspec_matchers.rb +21 -0
  193. data/lib/e11y/testing/snapshot_matcher.rb +86 -0
  194. data/lib/e11y/trace_context/sampler.rb +35 -0
  195. data/lib/e11y/tracing/faraday_middleware.rb +31 -0
  196. data/lib/e11y/tracing/net_http_patch.rb +33 -0
  197. data/lib/e11y/tracing/propagator.rb +116 -0
  198. data/lib/e11y/tracing.rb +47 -0
  199. data/lib/e11y/version.rb +1 -1
  200. data/lib/e11y/versioning/version_extractor.rb +32 -0
  201. data/lib/e11y.rb +141 -265
  202. data/lib/generators/e11y/event/event_generator.rb +22 -0
  203. data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
  204. data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
  205. data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
  206. data/lib/generators/e11y/install/install_generator.rb +34 -0
  207. data/lib/generators/e11y/install/templates/e11y.rb +239 -0
  208. data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
  209. data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
  210. data/lib/tasks/e11y_docs.rake +30 -0
  211. data/lib/tasks/e11y_events.rake +71 -0
  212. data/lib/tasks/e11y_lint.rake +91 -0
  213. data/lib/tasks/e11y_slo.rake +29 -0
  214. metadata +129 -39
  215. data/docs/ADR-010-developer-experience.md +0 -2166
  216. data/docs/API-REFERENCE-L28.md +0 -914
  217. data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
  218. data/docs/CONTRIBUTING.md +0 -312
  219. data/docs/IMPLEMENTATION_NOTES.md +0 -2804
  220. data/docs/IMPLEMENTATION_PLAN.md +0 -1971
  221. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
  222. data/docs/PLAN.md +0 -148
  223. data/docs/README.md +0 -296
  224. data/docs/design/00-memory-optimization.md +0 -593
  225. data/docs/guides/MIGRATION-L27-L28.md +0 -692
  226. data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
  227. data/docs/guides/README.md +0 -44
  228. data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
  229. data/lib/e11y/adapters/registry.rb +0 -141
  230. /data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +0 -0
@@ -21,7 +21,7 @@
21
21
  - 4.1. [Global Rate Limiting](#41-global-rate-limiting)
22
22
  - 4.2. [Per-Event Rate Limiting](#42-per-event-rate-limiting)
23
23
  - 4.3. [Per-Context Rate Limiting](#43-per-context-rate-limiting)
24
- - 4.4. [Redis Integration](#44-redis-integration)
24
+ - 4.4. [In-Memory Token Bucket Implementation](#44-in-memory-token-bucket-implementation)
25
25
  5. [Audit Trail](#5-audit-trail)
26
26
  - 5.1. [Immutable Events](#51-immutable-events)
27
27
  - 5.2. [Cryptographic Signing](#52-cryptographic-signing)
@@ -121,12 +121,10 @@ C4Context
121
121
 
122
122
  System(e11y, "E11y Gem", "Event tracking with security")
123
123
 
124
- System_Ext(redis, "Redis", "Rate limiting state")
125
124
  System_Ext(kms, "KMS", "Signing keys")
126
125
  System_Ext(audit_store, "Audit Store", "Immutable audit logs")
127
126
 
128
127
  Rel(dev, e11y, "Tracks events", "Events::OrderPaid.track(...)")
129
- Rel(e11y, redis, "Check rate limits", "Redis commands")
130
128
  Rel(e11y, kms, "Sign audit events", "HMAC-SHA256")
131
129
  Rel(e11y, audit_store, "Store signed events", "Append-only")
132
130
 
@@ -151,14 +149,12 @@ graph TB
151
149
  PII --> PerAdapter[Per-Adapter Rules]
152
150
  end
153
151
 
154
- subgraph "Rate Limiting"
155
- Rate --> Global[Global Limiter]
156
- Rate --> PerEvent[Per-Event Limiter]
157
- Rate --> PerContext[Per-Context Limiter]
152
+ subgraph "Rate Limiting (In-Memory)"
153
+ Rate --> Global[Global Token Bucket]
154
+ Rate --> PerEvent[Per-Event Token Buckets]
155
+ Rate --> PerContext[Per-Context Token Buckets]
158
156
 
159
- Global --> Redis1[Redis]
160
- PerEvent --> Redis2[Redis]
161
- PerContext --> Redis3[Redis]
157
+ Note1[In-Memory<br/>No Redis Dependency]
162
158
  end
163
159
 
164
160
  subgraph "Audit Trail"
@@ -239,19 +235,19 @@ sequenceDiagram
239
235
 
240
236
  #### 3.0.2. Three-Tier Filtering Strategy
241
237
 
242
- E11y uses a **3-tier approach** to balance security and performance:
238
+ E11y uses a **3-tier approach** to balance security and performance. Code uses `pii_filtering_mode` returning `:no_pii`, `:rails_filters`, or `:explicit_pii`:
243
239
 
244
- | Tier | Strategy | Cost | Use Case | Events/sec |
240
+ | Mode | Strategy | Cost | Use Case | Events/sec |
245
241
  |------|----------|------|----------|------------|
246
- | **Tier 1** | Skip filtering | 0ms | Health checks, metrics, internal events | 500 |
247
- | **Tier 2** | Rails filters only | ~0.05ms | Standard events (known PII keys) | 400 |
248
- | **Tier 3** | Deep filtering | ~0.2ms | User data, payments, complex nested | 100 |
242
+ | **:no_pii** | Skip filtering | 0ms | Health checks, metrics, internal events | 500 |
243
+ | **:rails_filters** | Rails filters only | ~0.05ms | Standard events (known PII keys) | 400 |
244
+ | **:explicit_pii** | Deep filtering | ~0.2ms | User data, payments, complex nested | 100 |
249
245
 
250
246
  **Performance Budget:**
251
247
  ```
252
- 500 events/sec × 0ms = 0ms CPU/sec (Tier 1)
253
- 400 events/sec × 0.05ms = 20ms CPU/sec (Tier 2)
254
- 100 events/sec × 0.2ms = 20ms CPU/sec (Tier 3)
248
+ 500 events/sec × 0ms = 0ms CPU/sec (:no_pii)
249
+ 400 events/sec × 0.05ms = 20ms CPU/sec (:rails_filters)
250
+ 100 events/sec × 0.2ms = 20ms CPU/sec (:explicit_pii)
255
251
  ---
256
252
  Total: 40ms CPU/sec = 4% CPU on single core ✅
257
253
  ```
@@ -268,7 +264,7 @@ class Events::HealthCheck < E11y::Event::Base
268
264
  end
269
265
 
270
266
  # ✅ Explicit: This event contains NO PII
271
- contains_pii false # Skip all PII filtering (Tier 1)
267
+ contains_pii false # Skip all PII filtering (:no_pii)
272
268
  end
273
269
  ```
274
270
 
@@ -280,7 +276,7 @@ class Events::OrderCreated < E11y::Event::Base
280
276
  required(:amount).filled(:float)
281
277
  end
282
278
 
283
- # No declaration → Rails filters applied (Tier 2, default)
279
+ # No declaration → Rails filters applied (:rails_filters, default)
284
280
  # Uses: Rails.application.config.filter_parameters
285
281
  end
286
282
  ```
@@ -296,7 +292,7 @@ class Events::UserRegistered < E11y::Event::Base
296
292
  end
297
293
 
298
294
  # ✅ Explicit: This event contains PII
299
- contains_pii true # Tier 3: Deep filtering
295
+ contains_pii true # :explicit_pii: Deep filtering
300
296
 
301
297
  pii_filtering do
302
298
  # ✅ MANDATORY: Explicit declaration for EACH schema field
@@ -389,7 +385,7 @@ module E11y
389
385
  module Linters
390
386
  class PiiDeclarationLinter
391
387
  def self.validate_all!
392
- E11y::Registry.all_events.each do |event_class|
388
+ E11y::Registry.event_classes.each do |event_class|
393
389
  validate!(event_class)
394
390
  end
395
391
  end
@@ -480,7 +476,7 @@ namespace :e11y do
480
476
  errors = []
481
477
  warnings = []
482
478
 
483
- E11y::Registry.all_events.each do |event_class|
479
+ E11y::Registry.event_classes.each do |event_class|
484
480
  begin
485
481
  E11y::Linters::PiiDeclarationLinter.validate!(event_class)
486
482
 
@@ -567,7 +563,7 @@ end
567
563
  #### 3.0.6. Default Behavior
568
564
 
569
565
  **If `contains_pii` not specified:**
570
- - ✅ Apply Rails filters only (Tier 2)
566
+ - ✅ Apply Rails filters only (:rails_filters)
571
567
  - ✅ Fast (0.05ms overhead)
572
568
  - ✅ Covers 90% of use cases (password, token, secret, api_key)
573
569
  - ⚠️ No linter validation (assumes you know what you're doing)
@@ -639,7 +635,10 @@ end
639
635
  └─→ PII Filtering (per-adapter, different rules per destination)
640
636
  ```
641
637
 
642
- **Key Insight:** PII filtering is **NOT a global middleware** — it's applied **inside each adapter** with different rules!
638
+ **Key Insight (Hybrid Implementation):** PII filtering uses a **hybrid model**:
639
+ - **:no_pii / :rails_filters:** Global middleware (PIIFilter) — skip or Rails filters only
640
+ - **:explicit_pii without exclude_adapters:** Global middleware — single filtered payload
641
+ - **:explicit_pii with exclude_adapters:** Per-adapter — PIIFilter produces `payload_rewrites`, Routing merges overrides per adapter (e.g. audit gets original for GDPR Art. 6(1)(c))
643
642
 
644
643
  ### 3.1. Rails Integration
645
644
 
@@ -1908,7 +1907,7 @@ RSpec.describe 'PII Filtering', type: :integration do
1908
1907
  end
1909
1908
  end
1910
1909
 
1911
- describe 'Tier 1: No PII (Skip Filtering)' do
1910
+ describe ':no_pii (Skip Filtering)' do
1912
1911
  let(:event_class) { Events::HealthCheck }
1913
1912
  let(:payload) { { status: 'ok' } }
1914
1913
 
@@ -1924,7 +1923,7 @@ RSpec.describe 'PII Filtering', type: :integration do
1924
1923
  end
1925
1924
  end
1926
1925
 
1927
- describe 'Tier 2: Rails Filters Only' do
1926
+ describe ':rails_filters (Rails Filters Only)' do
1928
1927
  let(:event_class) { Events::OrderCreated }
1929
1928
  let(:payload) do
1930
1929
  {
@@ -1950,7 +1949,7 @@ RSpec.describe 'PII Filtering', type: :integration do
1950
1949
  end
1951
1950
  end
1952
1951
 
1953
- describe 'Tier 3: Deep Filtering' do
1952
+ describe ':explicit_pii (Deep Filtering)' do
1954
1953
  let(:event_class) { Events::UserRegistered }
1955
1954
  let(:payload) do
1956
1955
  {
@@ -1994,9 +1993,9 @@ RSpec.describe 'PII Filtering', type: :integration do
1994
1993
  describe 'Performance Regression Test' do
1995
1994
  it 'meets performance budget for 1000 events/sec' do
1996
1995
  events = [
1997
- { class: Events::HealthCheck, payload: { status: 'ok' }, count: 500 }, # Tier 1
1998
- { class: Events::OrderCreated, payload: { id: 'o1' }, count: 400 }, # Tier 2
1999
- { class: Events::UserLogin, payload: { email: 'u@x.com' }, count: 100 } # Tier 3
1996
+ { class: Events::HealthCheck, payload: { status: 'ok' }, count: 500 }, # :no_pii
1997
+ { class: Events::OrderCreated, payload: { id: 'o1' }, count: 400 }, # :rails_filters
1998
+ { class: Events::UserLogin, payload: { email: 'u@x.com' }, count: 100 } # :explicit_pii
2000
1999
  ]
2001
2000
 
2002
2001
  start = Time.now
@@ -2112,7 +2111,7 @@ end
2112
2111
  bundle exec rspec spec/lib/e11y/security/pii_filtering_spec.rb
2113
2112
 
2114
2113
  # Test specific tier
2115
- bundle exec rspec spec/lib/e11y/security/pii_filtering_spec.rb -e "Tier 3"
2114
+ bundle exec rspec spec/e11y/middleware/pii_filtering_spec.rb -e "explicit_pii"
2116
2115
 
2117
2116
  # Performance test
2118
2117
  bundle exec rspec spec/lib/e11y/security/pii_filtering_spec.rb -e "Performance"
@@ -2183,31 +2182,65 @@ module E11y
2183
2182
  def initialize(config)
2184
2183
  @limit = config.limit # events per second
2185
2184
  @window = config.window || 1.second
2186
- @strategy = config.strategy || :sliding_window
2187
- @redis = config.redis
2188
2185
 
2189
- @counter = initialize_counter
2186
+ # In-memory token bucket (no Redis dependency)
2187
+ @bucket = TokenBucket.new(
2188
+ capacity: @limit,
2189
+ refill_rate: @limit,
2190
+ window: @window.to_f
2191
+ )
2190
2192
  end
2191
2193
 
2192
2194
  def allow?(event_data = nil)
2193
- @counter.increment('global')
2194
-
2195
- current_rate = @counter.rate('global', window: @window)
2196
-
2197
- current_rate <= @limit
2195
+ @bucket.allow?
2198
2196
  end
2199
2197
 
2200
2198
  def current_rate
2201
- @counter.rate('global', window: @window)
2199
+ # Calculate current rate based on tokens consumed
2200
+ @limit - @bucket.tokens
2202
2201
  end
2203
2202
 
2204
2203
  private
2205
2204
 
2206
- def initialize_counter
2207
- if @redis
2208
- RedisCounter.new(@redis, strategy: @strategy)
2209
- else
2210
- InMemoryCounter.new(strategy: @strategy)
2205
+ class TokenBucket
2206
+ def initialize(capacity:, refill_rate:, window:)
2207
+ @capacity = capacity
2208
+ @refill_rate = refill_rate
2209
+ @window = window
2210
+ @tokens = capacity.to_f
2211
+ @last_refill = Time.now
2212
+ @mutex = Mutex.new
2213
+ end
2214
+
2215
+ def allow?
2216
+ @mutex.synchronize do
2217
+ refill_tokens
2218
+ if @tokens >= 1.0
2219
+ @tokens -= 1.0
2220
+ true
2221
+ else
2222
+ false
2223
+ end
2224
+ end
2225
+ end
2226
+
2227
+ def tokens
2228
+ @mutex.synchronize do
2229
+ refill_tokens
2230
+ @tokens
2231
+ end
2232
+ end
2233
+
2234
+ private
2235
+
2236
+ def refill_tokens
2237
+ now = Time.now
2238
+ elapsed = now - @last_refill
2239
+ return if elapsed <= 0
2240
+
2241
+ tokens_to_add = elapsed * @refill_rate
2242
+ @tokens = [@tokens + tokens_to_add, @capacity].min
2243
+ @last_refill = now
2211
2244
  end
2212
2245
  end
2213
2246
  end
@@ -2252,9 +2285,18 @@ module E11y
2252
2285
  @limits = config.limits || {}
2253
2286
  @default_limit = config.default_limit || Float::INFINITY
2254
2287
  @window = config.window || 1.second
2255
- @redis = config.redis
2256
2288
 
2257
- @counter = initialize_counter
2289
+ # In-memory token buckets per event type (lazy initialization)
2290
+ @buckets = Hash.new do |hash, event_name|
2291
+ limit = @limits[event_name] || @default_limit
2292
+ next nil if limit == Float::INFINITY
2293
+ hash[event_name] = TokenBucket.new(
2294
+ capacity: limit,
2295
+ refill_rate: limit,
2296
+ window: @window.to_f
2297
+ )
2298
+ end
2299
+ @mutex = Mutex.new
2258
2300
  end
2259
2301
 
2260
2302
  def allow?(event_data)
@@ -2263,15 +2305,15 @@ module E11y
2263
2305
 
2264
2306
  return true if limit == Float::INFINITY
2265
2307
 
2266
- @counter.increment(event_name)
2267
-
2268
- current_rate = @counter.rate(event_name, window: @window)
2269
-
2270
- current_rate <= limit
2308
+ bucket = @mutex.synchronize { @buckets[event_name] }
2309
+ bucket&.allow? || false
2271
2310
  end
2272
2311
 
2273
2312
  def current_rate(event_name)
2274
- @counter.rate(event_name, window: @window)
2313
+ bucket = @mutex.synchronize { @buckets[event_name] }
2314
+ return Float::INFINITY unless bucket
2315
+ limit = @limits[event_name] || @default_limit
2316
+ limit - bucket.tokens
2275
2317
  end
2276
2318
  end
2277
2319
  end
@@ -2316,9 +2358,16 @@ module E11y
2316
2358
  @limit = config.limit
2317
2359
  @window = config.window || 1.minute
2318
2360
  @context_keys = config.context_keys || [:user_id]
2319
- @redis = config.redis
2320
2361
 
2321
- @counter = initialize_counter
2362
+ # In-memory token buckets per context value (lazy initialization)
2363
+ @buckets = Hash.new do |hash, context_value|
2364
+ hash[context_value] = TokenBucket.new(
2365
+ capacity: @limit,
2366
+ refill_rate: @limit,
2367
+ window: @window.to_f
2368
+ )
2369
+ end
2370
+ @mutex = Mutex.new
2322
2371
  end
2323
2372
 
2324
2373
  def allow?(event_data)
@@ -2326,12 +2375,8 @@ module E11y
2326
2375
 
2327
2376
  return true unless context_value
2328
2377
 
2329
- key = "context:#{context_value}"
2330
- @counter.increment(key)
2331
-
2332
- current_rate = @counter.rate(key, window: @window)
2333
-
2334
- current_rate <= @limit
2378
+ bucket = @mutex.synchronize { @buckets[context_value] }
2379
+ bucket.allow?
2335
2380
  end
2336
2381
 
2337
2382
  private
@@ -2376,88 +2421,58 @@ end
2376
2421
 
2377
2422
  ---
2378
2423
 
2379
- ### 4.4. Redis Integration
2424
+ ### 4.4. In-Memory Token Bucket Implementation
2425
+
2426
+ **Design Decision:** In-memory token bucket algorithm (no Redis dependency).
2380
2427
 
2381
- **Design Decision:** Distributed rate limiting with Redis.
2428
+ **Rationale:**
2429
+ - ✅ **Fast:** No network latency (O(1) operations, ~0.003ms per event)
2430
+ - ✅ **Simple:** No external dependencies (Redis not required)
2431
+ - ✅ **Thread-safe:** Mutex-protected token buckets
2432
+ - ✅ **Smooth rate limiting:** Token bucket avoids bursty behavior
2433
+ - ⚠️ **Trade-off:** Per-process limits (not shared across instances)
2382
2434
 
2435
+ **Current Implementation:**
2383
2436
  ```ruby
2384
2437
  module E11y
2385
- module RateLimiting
2386
- class RedisCounter
2387
- def initialize(redis, strategy: :sliding_window)
2388
- @redis = redis
2389
- @strategy = strategy
2390
- end
2391
-
2392
- def increment(key)
2393
- case @strategy
2394
- when :sliding_window
2395
- increment_sliding_window(key)
2396
- when :fixed_window
2397
- increment_fixed_window(key)
2398
- when :token_bucket
2399
- increment_token_bucket(key)
2400
- end
2401
- end
2402
-
2403
- def rate(key, window:)
2404
- case @strategy
2405
- when :sliding_window
2406
- count_sliding_window(key, window)
2407
- when :fixed_window
2408
- count_fixed_window(key, window)
2409
- when :token_bucket
2410
- tokens_remaining(key)
2411
- end
2412
- end
2413
-
2414
- private
2415
-
2416
- # Sliding Window (most accurate)
2417
- def increment_sliding_window(key)
2418
- now = Time.now.to_f
2419
- redis_key = "e11y:rate:#{key}"
2420
-
2421
- @redis.multi do |multi|
2422
- multi.zadd(redis_key, now, "#{now}:#{SecureRandom.uuid}")
2423
- multi.expire(redis_key, 60)
2424
- end
2425
- end
2426
-
2427
- def count_sliding_window(key, window)
2428
- now = Time.now.to_f
2429
- cutoff = now - window.to_f
2430
- redis_key = "e11y:rate:#{key}"
2431
-
2432
- # Remove old entries
2433
- @redis.zremrangebyscore(redis_key, '-inf', cutoff)
2434
-
2435
- # Count remaining
2436
- @redis.zcard(redis_key)
2437
- end
2438
-
2439
- # Fixed Window (simpler, less accurate)
2440
- def increment_fixed_window(key)
2441
- window_key = current_window_key(key)
2442
-
2443
- @redis.multi do |multi|
2444
- multi.incr(window_key)
2445
- multi.expire(window_key, 60)
2438
+ module Middleware
2439
+ class RateLimiting < Base
2440
+ def initialize(app, global_limit: 10_000, per_event_limit: 1_000, window: 1.0)
2441
+ super(app)
2442
+ @global_limit = global_limit
2443
+ @per_event_limit = per_event_limit
2444
+ @window = window
2445
+
2446
+ # Token buckets for rate limiting (in-memory)
2447
+ @global_bucket = TokenBucket.new(
2448
+ capacity: @global_limit,
2449
+ refill_rate: @global_limit,
2450
+ window: @window
2451
+ )
2452
+ @per_event_buckets = Hash.new do |hash, event_name|
2453
+ hash[event_name] = TokenBucket.new(
2454
+ capacity: @per_event_limit,
2455
+ refill_rate: @per_event_limit,
2456
+ window: @window
2457
+ )
2446
2458
  end
2459
+
2460
+ @mutex = Mutex.new
2447
2461
  end
2448
-
2449
- def count_fixed_window(key, window)
2450
- window_key = current_window_key(key)
2451
- @redis.get(window_key).to_i
2452
- end
2453
-
2454
- def current_window_key(key)
2455
- window_start = (Time.now.to_i / 1) * 1 # 1-second windows
2456
- "e11y:rate:fixed:#{key}:#{window_start}"
2457
- end
2458
-
2459
- # Token Bucket (burst-friendly)
2460
- def increment_token_bucket(key)
2462
+ end
2463
+ end
2464
+ end
2465
+ ```
2466
+
2467
+ **Why Not Redis?**
2468
+ - ❌ **Complexity:** Adds external dependency and infrastructure overhead
2469
+ - **Latency:** Network round-trip adds ~0.25ms per event (83x slower)
2470
+ - ❌ **Not needed:** Per-process rate limiting is sufficient for event tracking
2471
+ - ✅ **User feedback:** "outdated solution"
2472
+
2473
+ **Note:** In-memory rate limiting is sufficient for most use cases. Each application process maintains its own rate limits, which is appropriate for event tracking workloads. If distributed rate limiting is needed in the future, it can be added as an optional feature.
2474
+
2475
+ ---
2461
2476
  redis_key = "e11y:rate:bucket:#{key}"
2462
2477
 
2463
2478
  # Lua script for atomic token consumption
@@ -3213,44 +3228,24 @@ end
3213
3228
  ```ruby
3214
3229
  # config/initializers/e11y.rb
3215
3230
  E11y.configure do |config|
3216
- config.security.baggage_protection do
3217
- enabled true # ✅ CRITICAL: Always enable in production
3218
-
3219
- # Allowlist: Only these keys allowed in baggage
3220
- allowed_keys [
3221
- 'trace_id',
3222
- 'span_id',
3223
- 'environment',
3224
- 'version',
3225
- 'service_name',
3226
- 'deployment_id',
3227
- 'request_id',
3228
- # Custom safe keys (optional):
3229
- 'feature_flag_id', # Feature flag identifier (not PII)
3230
- 'ab_test_variant' # A/B test variant (not PII)
3231
- ]
3232
-
3233
- # Block mode (how to handle violations)
3234
- block_mode :silent # Options: :silent, :warn, :raise
3235
-
3236
- # Monitoring
3237
- on_blocked_key do |key, value, caller_location|
3238
- # Track violations for security audit
3239
- Yabeda.e11y_baggage_pii_blocked.increment(
3240
- key: key,
3241
- service: ENV['SERVICE_NAME']
3242
- )
3243
-
3244
- # Alert on critical violations
3245
- if key.match?(/email|password|ssn|credit_card/)
3246
- Sentry.capture_message(
3247
- "Critical PII blocked from baggage: #{key}",
3248
- level: :warning,
3249
- extra: { caller: caller_location }
3250
- )
3251
- end
3252
- end
3253
- end
3231
+ config.security_baggage_protection_enabled = true # ✅ CRITICAL: Always enable in production
3232
+
3233
+ # Allowlist: Only these keys allowed in baggage
3234
+ config.security_baggage_protection_allowed_keys = [
3235
+ 'trace_id',
3236
+ 'span_id',
3237
+ 'environment',
3238
+ 'version',
3239
+ 'service_name',
3240
+ 'deployment_id',
3241
+ 'request_id',
3242
+ # Custom safe keys (optional):
3243
+ 'feature_flag_id', # Feature flag identifier (not PII)
3244
+ 'ab_test_variant' # A/B test variant (not PII)
3245
+ ]
3246
+
3247
+ # Block mode (how to handle violations)
3248
+ config.security_baggage_protection_block_mode = :silent # Options: :silent, :warn, :raise
3254
3249
  end
3255
3250
  ```
3256
3251
 
@@ -3314,14 +3309,12 @@ OpenTelemetry::Baggage.set_value('request_id', SecureRandom.uuid)
3314
3309
  ```ruby
3315
3310
  # config/environments/development.rb
3316
3311
  E11y.configure do |config|
3317
- config.security.baggage_protection do
3318
- enabled true
3319
-
3320
- # RAISE exception on blocked keys (fail fast)
3321
- block_mode :raise # ← Developer sees error immediately
3322
-
3323
- allowed_keys E11y::Middleware::BaggageProtection::ALLOWED_KEYS
3324
- end
3312
+ config.security_baggage_protection_enabled = true
3313
+
3314
+ # RAISE exception on blocked keys (fail fast)
3315
+ config.security_baggage_protection_block_mode = :raise # Developer sees error immediately
3316
+
3317
+ config.security_baggage_protection_allowed_keys = E11y::BAGGAGE_PROTECTION_DEFAULT_ALLOWED_KEYS
3325
3318
  end
3326
3319
 
3327
3320
  # Developer tries to set PII in baggage:
@@ -3426,11 +3419,11 @@ Events::UserLogin.track(
3426
3419
 
3427
3420
  **Metadata Flags:**
3428
3421
  - `:pii_filtered` - Event already went through PII filtering (set by PII filter middleware)
3429
- - `:replayed` - Event is being replayed from DLQ (set by DLQ replay service)
3422
+ - `:dlq_replayed` - Event is being replayed from DLQ (set by DLQ replay service)
3430
3423
 
3431
3424
  **When to Skip PII Filtering:**
3432
3425
  1. Event has `:pii_filtered => true` (already processed)
3433
- 2. Event has `:replayed => true` (replay scenario)
3426
+ 2. Event has `:dlq_replayed => true` (replay scenario)
3434
3427
  3. Both flags present → skip PII filtering entirely
3435
3428
 
3436
3429
  ### 5.6.3. PiiFilter Middleware with Replay Detection
@@ -3457,7 +3450,7 @@ module E11y
3457
3450
 
3458
3451
  # Check if event contains PII
3459
3452
  unless event_class.contains_pii?
3460
- # No PII declared, skip filtering (Tier 1)
3453
+ # No PII declared, skip filtering (:no_pii)
3461
3454
  return event_data
3462
3455
  end
3463
3456
 
@@ -3477,7 +3470,7 @@ module E11y
3477
3470
  metadata = event_data[:metadata] || {}
3478
3471
 
3479
3472
  # Check for replay flag
3480
- return true if metadata[:replayed]
3473
+ return true if metadata[:dlq_replayed]
3481
3474
 
3482
3475
  # Check for already-filtered flag
3483
3476
  return true if metadata[:pii_filtered]
@@ -3507,7 +3500,7 @@ module E11y
3507
3500
 
3508
3501
  # ✅ CRITICAL: Mark as replayed (skip transformations)
3509
3502
  event_data[:metadata] ||= {}
3510
- event_data[:metadata][:replayed] = true
3503
+ event_data[:metadata][:dlq_replayed] = true
3511
3504
  event_data[:metadata][:pii_filtered] = true # Already filtered!
3512
3505
  event_data[:metadata][:replayed_at] = Time.now.utc.iso8601
3513
3506
  event_data[:metadata][:original_event_id] = event_data[:event_id]
@@ -3560,7 +3553,7 @@ E11y.configure do |config|
3560
3553
  config.dlq.replay do
3561
3554
  # Metadata flags for replayed events
3562
3555
  set_metadata_flags do
3563
- replayed true
3556
+ dlq_replayed true
3564
3557
  pii_filtered true
3565
3558
  replay_timestamp true
3566
3559
  original_event_id true
@@ -4022,7 +4015,7 @@ RSpec.describe E11y::RateLimiting do
4022
4015
  end
4023
4016
 
4024
4017
  # Exceed limit
4025
- E11y.config.rate_limiting.global.limit = 100
4018
+ E11y.config.rate_limiting_global_limit = 100
4026
4019
 
4027
4020
  expect(Events::Test.track(message: 'over limit')).to be_falsey
4028
4021
  end
@@ -127,25 +127,7 @@ C4Context
127
127
 
128
128
  **Configuration:**
129
129
 
130
- ```ruby
131
- # config/initializers/e11y.rb
132
- E11y.configure do |config|
133
- # Option 1: Yabeda only (DEFAULT, recommended for Rails)
134
- config.metrics do
135
- backend :yabeda # Prometheus via Yabeda
136
- end
137
-
138
- # Option 2: OpenTelemetry only (for OTLP backends)
139
- # config.metrics do
140
- # backend :opentelemetry # OTLP via OTel SDK
141
- # end
142
-
143
- # Option 3: Both (for migration period ONLY)
144
- # config.metrics do
145
- # backend [:yabeda, :opentelemetry] # ⚠️ DOUBLE OVERHEAD!
146
- # end
147
- end
148
- ```
130
+ Metrics are emitted via Yabeda adapter when events with `metrics do` are tracked. Register Yabeda adapter in `config.adapters`.
149
131
 
150
132
  **Metrics Adapter Pattern:**
151
133
 
@@ -756,7 +738,7 @@ module E11y
756
738
  return true if event[:severity].in?([:error, :fatal])
757
739
 
758
740
  # Check if event matches span creation patterns
759
- E11y.config.opentelemetry.span_creation_patterns.any? do |pattern|
741
+ E11y.config.opentelemetry_span_creation_patterns.any? do |pattern|
760
742
  File.fnmatch(pattern, event[:event_name])
761
743
  end
762
744
  end
@@ -1068,7 +1050,7 @@ module E11y
1068
1050
  return true if event[:severity].in?([:error, :fatal])
1069
1051
 
1070
1052
  # Check configured patterns
1071
- patterns = E11y.config.opentelemetry.span_creation_patterns || []
1053
+ patterns = E11y.config.opentelemetry_span_creation_patterns || []
1072
1054
  patterns.any? { |pattern| File.fnmatch(pattern, event[:event_name]) }
1073
1055
  end
1074
1056