e11y 0.2.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (288) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +130 -10
  3. data/CHANGELOG.md +80 -1
  4. data/CLAUDE.md +168 -0
  5. data/CONTRIBUTING.md +640 -0
  6. data/README.md +165 -701
  7. data/RELEASE.md +41 -12
  8. data/Rakefile +249 -57
  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 +79 -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} +36 -65
  30. data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
  31. data/docs/architecture/ADR-003-slo-observability.md +1402 -0
  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} +182 -743
  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} +44 -86
  40. data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +11 -11
  41. data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
  42. data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
  43. data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +43 -59
  44. data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +58 -355
  45. data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
  46. data/docs/architecture/ADR-018-memory-optimization.md +366 -0
  47. data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
  48. data/docs/plans/2026-03-20-browser-overlay-svelte.md +281 -0
  49. data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
  50. data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
  51. data/docs/prd/01-overview-vision.md +19 -14
  52. data/docs/use_cases/README.md +22 -23
  53. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
  54. data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
  55. data/docs/use_cases/UC-003-event-metrics.md +66 -0
  56. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +33 -684
  57. data/docs/use_cases/UC-005-sentry-integration.md +13 -15
  58. data/docs/use_cases/UC-006-trace-context-management.md +30 -28
  59. data/docs/use_cases/UC-007-pii-filtering.md +35 -87
  60. data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
  61. data/docs/use_cases/UC-009-multi-service-tracing.md +30 -178
  62. data/docs/use_cases/UC-010-background-job-tracking.md +24 -91
  63. data/docs/use_cases/UC-011-rate-limiting.md +95 -168
  64. data/docs/use_cases/UC-012-audit-trail.md +21 -46
  65. data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
  66. data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
  67. data/docs/use_cases/UC-015-cost-optimization.md +46 -99
  68. data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
  69. data/docs/use_cases/UC-017-local-development.md +203 -777
  70. data/docs/use_cases/UC-018-testing-events.md +3 -3
  71. data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
  72. data/docs/use_cases/UC-020-event-versioning.md +8 -9
  73. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
  74. data/docs/use_cases/UC-022-event-registry.md +15 -21
  75. data/docs/use_cases/backlog.md +119 -87
  76. data/e11y.gemspec +2 -2
  77. data/gems/e11y-devtools/README.md +158 -0
  78. data/gems/e11y-devtools/config/routes.rb +15 -0
  79. data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
  80. data/gems/e11y-devtools/exe/e11y +34 -0
  81. data/gems/e11y-devtools/frontend/.gitignore +24 -0
  82. data/gems/e11y-devtools/frontend/README.md +51 -0
  83. data/gems/e11y-devtools/frontend/index.html +14 -0
  84. data/gems/e11y-devtools/frontend/package-lock.json +3707 -0
  85. data/gems/e11y-devtools/frontend/package.json +28 -0
  86. data/gems/e11y-devtools/frontend/public/mocks/v1/events/recent.json +4205 -0
  87. data/gems/e11y-devtools/frontend/public/mocks/v1/interactions.json +194 -0
  88. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0a2e04027cfa22d014bc22e8b27cd913/events.json +86 -0
  89. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0e1543af6a630fb3af6b52283154b3e0/events.json +169 -0
  90. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/1838b691faa49564f97db8592ff3978d/events.json +78 -0
  91. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/29f198f6588dacffb687777eb5f8f118/events.json +197 -0
  92. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/34bc3c9c0097de28a7a6f99b90a8e7bc/events.json +194 -0
  93. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/3ba6c20d068ab9cee00e51b180e66444/events.json +184 -0
  94. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/435bfd8f17b9009146a79812d7c3726d/events.json +144 -0
  95. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/4c7676e3fe668e99edb2b94d7d5678a9/events.json +222 -0
  96. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/6daf0d47974bedfc55d5de7004a3ea9f/events.json +194 -0
  97. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8a81ada42834d15f287bb40010043605/events.json +194 -0
  98. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8c0a98900edaae105469df8daedccf02/events.json +198 -0
  99. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8e4f645180f8a7d1dce426b07380466b/events.json +222 -0
  100. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/93db346fa5d44a032605a13b627f4b80/events.json +128 -0
  101. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/98ff6146faf7bd9be8bd03a8275817ba/events.json +223 -0
  102. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/9997ddd0247bc7e25f2ca7a5c415c93d/events.json +197 -0
  103. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/99e35f8ef3baedd798cc4fd085980ad9/events.json +194 -0
  104. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b4f3095c1909924cbc98889a86c83d6d/events.json +131 -0
  105. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b54b7fc32b7575a7110de809d11ccda0/events.json +128 -0
  106. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c0b48033fa06746bcc5886745e053cff/events.json +169 -0
  107. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c44649ac76701b4558927cd2305ab535/events.json +169 -0
  108. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/d601ae3320057580a39dbdac2edfdf4a/events.json +248 -0
  109. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e67e724bab422d2b52eeb49635e512e1/events.json +194 -0
  110. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e6c72765a28f158a8485b35fa63f73da/events.json +194 -0
  111. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/f541b87405c9a54819b18ebe529f6419/events.json +194 -0
  112. data/gems/e11y-devtools/frontend/scripts/generate_mocks.rb +397 -0
  113. data/gems/e11y-devtools/frontend/src/App.svelte +827 -0
  114. data/gems/e11y-devtools/frontend/src/components/Fab.svelte +19 -0
  115. data/gems/e11y-devtools/frontend/src/components/FilterBar.svelte +38 -0
  116. data/gems/e11y-devtools/frontend/src/components/FullscreenPanel.svelte +82 -0
  117. data/gems/e11y-devtools/frontend/src/components/InteractionsTimeline.svelte +264 -0
  118. data/gems/e11y-devtools/frontend/src/components/RecentHistogram.svelte +354 -0
  119. data/gems/e11y-devtools/frontend/src/lib/api.ts +37 -0
  120. data/gems/e11y-devtools/frontend/src/lib/eventIdentity.ts +12 -0
  121. data/gems/e11y-devtools/frontend/src/lib/format.ts +37 -0
  122. data/gems/e11y-devtools/frontend/src/lib/listFilter.ts +43 -0
  123. data/gems/e11y-devtools/frontend/src/lib/recentVolume.ts +80 -0
  124. data/gems/e11y-devtools/frontend/src/lib/router.ts +12 -0
  125. data/gems/e11y-devtools/frontend/src/lib/transitions.ts +34 -0
  126. data/gems/e11y-devtools/frontend/src/lib/viewportOrigin.ts +25 -0
  127. data/gems/e11y-devtools/frontend/src/main.ts +8 -0
  128. data/gems/e11y-devtools/frontend/src/overlay-entry.ts +24 -0
  129. data/gems/e11y-devtools/frontend/src/overlay.css +1080 -0
  130. data/gems/e11y-devtools/frontend/svelte.config.js +2 -0
  131. data/gems/e11y-devtools/frontend/test_puppeteer.js +41 -0
  132. data/gems/e11y-devtools/frontend/test_scale.js +3 -0
  133. data/gems/e11y-devtools/frontend/tsconfig.app.json +21 -0
  134. data/gems/e11y-devtools/frontend/tsconfig.json +7 -0
  135. data/gems/e11y-devtools/frontend/tsconfig.node.json +26 -0
  136. data/gems/e11y-devtools/frontend/vite.config.ts +36 -0
  137. data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
  138. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
  139. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
  140. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
  141. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
  142. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
  143. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
  144. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
  145. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
  146. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
  147. data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +20 -0
  148. data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +94 -0
  149. data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
  150. data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
  151. data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +67 -0
  152. data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
  153. data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
  154. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
  155. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
  156. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
  157. data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
  158. data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
  159. data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
  160. data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +91 -0
  161. data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
  162. data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
  163. data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
  164. data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
  165. data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
  166. data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
  167. data/lib/e11y/adapters/audit_encrypted.rb +53 -11
  168. data/lib/e11y/adapters/base.rb +33 -34
  169. data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
  170. data/lib/e11y/adapters/dev_log/query.rb +219 -0
  171. data/lib/e11y/adapters/dev_log.rb +118 -0
  172. data/lib/e11y/adapters/file.rb +3 -6
  173. data/lib/e11y/adapters/in_memory.rb +52 -5
  174. data/lib/e11y/adapters/in_memory_test.rb +29 -0
  175. data/lib/e11y/adapters/loki.rb +58 -23
  176. data/lib/e11y/adapters/null.rb +82 -0
  177. data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
  178. data/lib/e11y/adapters/otel_logs.rb +136 -23
  179. data/lib/e11y/adapters/sentry.rb +4 -7
  180. data/lib/e11y/adapters/stdout.rb +73 -7
  181. data/lib/e11y/adapters/yabeda.rb +153 -29
  182. data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
  183. data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
  184. data/lib/e11y/buffers/ring_buffer.rb +3 -16
  185. data/lib/e11y/configuration.rb +272 -0
  186. data/lib/e11y/console.rb +10 -17
  187. data/lib/e11y/current.rb +53 -1
  188. data/lib/e11y/debug/pipeline_inspector.rb +96 -0
  189. data/lib/e11y/documentation/generator.rb +48 -0
  190. data/lib/e11y/event/base.rb +176 -82
  191. data/lib/e11y/event/value_sampling_config.rb +1 -5
  192. data/lib/e11y/events/rails/database/query.rb +1 -4
  193. data/lib/e11y/events/rails/job/failed.rb +2 -0
  194. data/lib/e11y/instruments/active_job.rb +44 -12
  195. data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
  196. data/lib/e11y/instruments/sidekiq.rb +135 -31
  197. data/lib/e11y/linters/base.rb +11 -0
  198. data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
  199. data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
  200. data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
  201. data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
  202. data/lib/e11y/logger/bridge.rb +26 -7
  203. data/lib/e11y/metrics/cardinality_protection.rb +10 -15
  204. data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
  205. data/lib/e11y/metrics/registry.rb +3 -5
  206. data/lib/e11y/metrics/test_backend.rb +62 -0
  207. data/lib/e11y/metrics.rb +56 -10
  208. data/lib/e11y/middleware/adapter_resolver.rb +40 -0
  209. data/lib/e11y/middleware/audit_signing.rb +43 -6
  210. data/lib/e11y/middleware/baggage_protection.rb +75 -0
  211. data/lib/e11y/middleware/dev_log_source.rb +24 -0
  212. data/lib/e11y/middleware/event_slo.rb +23 -9
  213. data/lib/e11y/middleware/otel_span.rb +23 -0
  214. data/lib/e11y/middleware/pii_filter.rb +104 -75
  215. data/lib/e11y/middleware/rate_limiting.rb +54 -27
  216. data/lib/e11y/middleware/request.rb +70 -23
  217. data/lib/e11y/middleware/routing.rb +78 -21
  218. data/lib/e11y/middleware/sampling.rb +66 -17
  219. data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
  220. data/lib/e11y/middleware/trace_context.rb +45 -10
  221. data/lib/e11y/middleware/track_latency.rb +34 -0
  222. data/lib/e11y/middleware/validation.rb +7 -16
  223. data/lib/e11y/middleware/versioning.rb +26 -22
  224. data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
  225. data/lib/e11y/opentelemetry/span_creator.rb +142 -0
  226. data/lib/e11y/pii/patterns.rb +12 -1
  227. data/lib/e11y/pipeline/builder.rb +4 -4
  228. data/lib/e11y/presets/audit_event.rb +13 -2
  229. data/lib/e11y/railtie.rb +52 -14
  230. data/lib/e11y/registry.rb +306 -0
  231. data/lib/e11y/reliability/circuit_breaker.rb +19 -21
  232. data/lib/e11y/reliability/dlq/base.rb +71 -0
  233. data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
  234. data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
  235. data/lib/e11y/reliability/dlq/filter.rb +37 -54
  236. data/lib/e11y/reliability/retry_handler.rb +26 -29
  237. data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
  238. data/lib/e11y/sampling/error_spike_detector.rb +0 -2
  239. data/lib/e11y/sampling/load_monitor.rb +5 -9
  240. data/lib/e11y/sampling/stratified_tracker.rb +18 -0
  241. data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
  242. data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
  243. data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
  244. data/lib/e11y/slo/config_loader.rb +40 -0
  245. data/lib/e11y/slo/config_validator.rb +58 -0
  246. data/lib/e11y/slo/dashboard_generator.rb +122 -0
  247. data/lib/e11y/slo/event_driven.rb +8 -0
  248. data/lib/e11y/slo/tracker.rb +31 -4
  249. data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
  250. data/lib/e11y/testing/rspec_matchers.rb +21 -0
  251. data/lib/e11y/testing/snapshot_matcher.rb +86 -0
  252. data/lib/e11y/trace_context/sampler.rb +35 -0
  253. data/lib/e11y/tracing/faraday_middleware.rb +31 -0
  254. data/lib/e11y/tracing/net_http_patch.rb +33 -0
  255. data/lib/e11y/tracing/propagator.rb +144 -0
  256. data/lib/e11y/tracing.rb +47 -0
  257. data/lib/e11y/version.rb +1 -1
  258. data/lib/e11y/versioning/version_extractor.rb +32 -0
  259. data/lib/e11y.rb +123 -266
  260. data/lib/generators/e11y/event/event_generator.rb +22 -0
  261. data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
  262. data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
  263. data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
  264. data/lib/generators/e11y/install/install_generator.rb +34 -0
  265. data/lib/generators/e11y/install/templates/e11y.rb +239 -0
  266. data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
  267. data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
  268. data/lib/tasks/e11y_docs.rake +30 -0
  269. data/lib/tasks/e11y_events.rake +71 -0
  270. data/lib/tasks/e11y_lint.rake +91 -0
  271. data/lib/tasks/e11y_slo.rake +29 -0
  272. metadata +186 -39
  273. data/docs/ADR-003-slo-observability.md +0 -3337
  274. data/docs/ADR-010-developer-experience.md +0 -2166
  275. data/docs/API-REFERENCE-L28.md +0 -914
  276. data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
  277. data/docs/CONTRIBUTING.md +0 -312
  278. data/docs/IMPLEMENTATION_NOTES.md +0 -2804
  279. data/docs/IMPLEMENTATION_PLAN.md +0 -1971
  280. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
  281. data/docs/PLAN.md +0 -148
  282. data/docs/README.md +0 -296
  283. data/docs/design/00-memory-optimization.md +0 -593
  284. data/docs/guides/MIGRATION-L27-L28.md +0 -692
  285. data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
  286. data/docs/guides/README.md +0 -44
  287. data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
  288. data/lib/e11y/adapters/registry.rb +0 -141
@@ -69,23 +69,30 @@ module E11y
69
69
  # Skip if SLO not enabled for this event
70
70
  # Support explicit event_class (for testing) or resolve from event_name
71
71
  event_class = event_data[:event_class] || resolve_event_class(event_data)
72
- return event_data unless event_class.respond_to?(:slo_config)
73
- return event_data unless event_class.slo_config&.enabled?
72
+ unless event_class.respond_to?(:slo_config) && event_class.slo_config&.enabled?
73
+ # Pass to next middleware even if SLO not enabled
74
+ return @app&.call(event_data) || event_data
75
+ end
74
76
 
75
77
  # Compute slo_status from payload
76
78
  slo_status = compute_slo_status(event_class, event_data[:payload])
77
- return event_data unless slo_status
79
+ unless slo_status
80
+ # Pass to next middleware even if slo_status is nil
81
+ return @app&.call(event_data) || event_data
82
+ end
78
83
 
79
- # Emit SLO metric
80
- emit_slo_metric(event_class, slo_status, event_data[:payload])
84
+ # Emit SLO metric (with sampling correction when stratified sampling enabled)
85
+ emit_slo_metric(event_class, slo_status, event_data[:payload], event_data)
81
86
 
82
- event_data # Passthrough (never modify event_data)
87
+ # Pass to next middleware (Routing writes to adapters)
88
+ @app&.call(event_data) || event_data
83
89
  rescue StandardError => e
84
90
  # Never fail event tracking due to SLO processing
85
91
  E11y.logger.error(
86
92
  "[E11y::Middleware::EventSlo] SLO processing failed for #{event_data[:event_name]}: #{e.message}"
87
93
  )
88
- event_data
94
+ # Still pass to next middleware even on error
95
+ @app&.call(event_data) || event_data
89
96
  end
90
97
 
91
98
  private
@@ -124,15 +131,22 @@ module E11y
124
131
  end
125
132
 
126
133
  # Emit SLO metric to Yabeda/Prometheus.
134
+ # C11: Applies stratified sampling correction when event was sampled.
127
135
  #
128
136
  # @param event_class [Class] Event class
129
137
  # @param slo_status [String] 'success' or 'failure'
130
138
  # @param payload [Hash] Event payload
139
+ # @param event_data [Hash] Full event data (for sample_rate)
131
140
  # @return [void]
132
- def emit_slo_metric(event_class, slo_status, payload)
141
+ def emit_slo_metric(event_class, slo_status, payload, _event_data = {})
133
142
  labels = build_slo_labels(event_class, slo_status, payload)
134
143
 
135
- E11y::Metrics.increment(:slo_event_result_total, labels)
144
+ # C11: Apply sampling correction for accurate SLO with stratified sampling
145
+ stratum = slo_status == "success" ? :success : :error
146
+ correction = E11y::Sampling.stratified_tracker.sampling_correction(stratum)
147
+ value = (correction * 100).round / 100.0 # Round to 2 decimals for Prometheus
148
+
149
+ E11y::Metrics.increment(:slo_event_result_total, labels, value: value)
136
150
  rescue StandardError => e
137
151
  E11y.logger.error(
138
152
  "[E11y::Middleware::EventSlo] Failed to emit SLO metric for #{event_class.name}: #{e.message}"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Middleware
5
+ # OtelSpan middleware — creates OpenTelemetry spans from events (ADR-007 §6, F2).
6
+ #
7
+ # When config.opentelemetry_span_creation_patterns is set, creates OTel spans
8
+ # for matching events. Errors/fatal always create spans.
9
+ #
10
+ # @see E11y::OpenTelemetry::SpanCreator
11
+ # @see ADR-007 §6 Traces Signal Export
12
+ class OtelSpan < Base
13
+ middleware_zone :adapters
14
+
15
+ def call(event_data)
16
+ if defined?(::OpenTelemetry::Trace) && defined?(E11y::OpenTelemetry::SpanCreator)
17
+ E11y::OpenTelemetry::SpanCreator.create_span_from_event(event_data)
18
+ end
19
+ @app.call(event_data)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/parameter_filter"
4
+
3
5
  module E11y
4
6
  module Middleware
5
- # PII Filter Middleware - 3-Tier Strategy
7
+ # PII Filter Middleware
6
8
  #
7
9
  # Filters Personally Identifiable Information (PII) from event payloads
8
- # before they reach adapters. Implements ADR-006 3-tier security model.
10
+ # before they reach adapters. Implements ADR-006 security model.
9
11
  #
10
- # **Three-Tier Strategy:**
11
- # - Tier 1: No PII (`contains_pii false`) - Skip filtering (0ms overhead)
12
- # - Tier 2: Default - Rails filters only (~0.05ms overhead)
13
- # - Tier 3: Explicit PII (`contains_pii true`) - Deep filtering (~0.2ms overhead)
12
+ # **Filtering modes:**
13
+ # - :no_pii Skip filtering (contains_pii false, 0ms overhead)
14
+ # - :rails_filters Rails filter_parameters only (~0.05ms overhead)
15
+ # - :explicit_pii Field strategies, optionally per-adapter via exclude_adapters (~0.2ms)
14
16
  #
15
- # @example Basic Usage (Tier 2 - Default)
17
+ # @example Basic Usage (:rails_filters - default)
16
18
  # class Events::OrderCreated < E11y::Event::Base
17
19
  # schema do
18
20
  # required(:order_id).filled(:string)
@@ -20,12 +22,12 @@ module E11y
20
22
  # end
21
23
  # end
22
24
  #
23
- # @example Tier 1: No PII (High Performance)
25
+ # @example :no_pii (skip filtering)
24
26
  # class Events::HealthCheck < E11y::Event::Base
25
- # contains_pii false # Skip all filtering
27
+ # contains_pii false
26
28
  # end
27
29
  #
28
- # @example Tier 3: Explicit PII (Deep Filtering)
30
+ # @example :explicit_pii (field strategies)
29
31
  # class Events::UserRegistered < E11y::Event::Base
30
32
  # contains_pii true
31
33
  #
@@ -40,7 +42,6 @@ module E11y
40
42
  # @see UC-007 PII Filtering
41
43
  # @see E11y::PII::Patterns
42
44
  # rubocop:disable Metrics/ClassLength
43
- # PII filter is a cohesive security component with 3-tier filtering strategy
44
45
  class PIIFilter < Base
45
46
  middleware_zone :security
46
47
 
@@ -53,27 +54,24 @@ module E11y
53
54
  @config = config
54
55
  end
55
56
 
56
- # Process event and filter PII based on tier
57
+ # Process event and filter PII based on filtering mode
57
58
  #
58
59
  # @param event_data [Hash] Event data with payload
59
60
  # @return [Hash] Processed event data
60
61
  # rubocop:disable Lint/DuplicateBranch
61
- # Unknown tiers intentionally fallback to no filtering (same as tier1)
62
62
  def call(event_data)
63
- # Determine filtering tier
64
- tier = determine_tier(event_data)
63
+ return @app.call(event_data) if event_data[:dlq_replayed]
64
+
65
+ mode = filtering_mode(event_data)
65
66
 
66
- case tier
67
- when :tier1
68
- # Tier 1: No PII - Skip filtering (0ms overhead)
67
+ case mode
68
+ when :no_pii
69
69
  @app.call(event_data)
70
- when :tier2
71
- # Tier 2: Rails filters only (~0.05ms overhead)
70
+ when :rails_filters
72
71
  filtered_data = apply_rails_filters(event_data)
73
72
  @app.call(filtered_data)
74
- when :tier3
75
- # Tier 3: Deep filtering (~0.2ms overhead)
76
- filtered_data = apply_deep_filtering(event_data)
73
+ when :explicit_pii
74
+ filtered_data = apply_explicit_pii_filtering(event_data)
77
75
  @app.call(filtered_data)
78
76
  else
79
77
  @app.call(event_data)
@@ -83,16 +81,11 @@ module E11y
83
81
 
84
82
  private
85
83
 
86
- # Determine PII filtering tier for event
87
- #
88
- # @param event_data [Hash] Event data
89
- # @return [Symbol] :tier1, :tier2, or :tier3
90
- def determine_tier(event_data)
84
+ def filtering_mode(event_data)
91
85
  event_class = event_data[:event_class]
92
- return :tier2 unless event_class.respond_to?(:pii_tier)
86
+ return :rails_filters unless event_class.respond_to?(:pii_filtering_mode)
93
87
 
94
- # Return tier directly from event class
95
- event_class.pii_tier
88
+ event_class.pii_filtering_mode
96
89
  end
97
90
 
98
91
  # Apply Rails filter_parameters (Tier 2)
@@ -109,52 +102,71 @@ module E11y
109
102
  filtered_data
110
103
  end
111
104
 
112
- # Apply deep PII filtering (Tier 3)
113
- #
114
- # @param event_data [Hash] Event data
115
- # @return [Hash] Filtered event data
116
- def apply_deep_filtering(event_data)
105
+ # :explicit_pii field strategies, optionally payload_rewrites when exclude_adapters present.
106
+ def apply_explicit_pii_filtering(event_data)
117
107
  event_class = event_data[:event_class]
118
108
  return event_data unless event_class
119
109
 
120
- # Clone to avoid modifying original
121
- filtered_data = deep_dup(event_data)
122
-
123
- # Get PII filtering config from event class
124
110
  pii_config = event_class.pii_filtering_config if event_class.respond_to?(:pii_filtering_config)
125
- return filtered_data unless pii_config
111
+ return event_data unless pii_config
112
+
113
+ # 1. Base payload (most restrictive)
114
+ base_payload = apply_field_strategies(deep_dup(event_data[:payload]), pii_config, nil)
115
+ base_payload = apply_pattern_filtering(base_payload, pii_config, [])
126
116
 
127
- # Apply field-level strategies
128
- filtered_data[:payload] = apply_field_strategies(
129
- filtered_data[:payload],
130
- pii_config
131
- )
117
+ filtered_data = deep_dup(event_data)
118
+ filtered_data[:payload] = base_payload
132
119
 
133
- # Apply pattern-based filtering
134
- filtered_data[:payload] = apply_pattern_filtering(
135
- filtered_data[:payload]
136
- )
120
+ # 2. payload_rewrites: per-adapter overrides for exclude_adapters fields only
121
+ has_exclude_adapters = pii_config[:fields]&.any? { |_, v| v[:exclude_adapters]&.any? }
122
+ filtered_data[:payload_rewrites] = build_payload_rewrites(event_data, pii_config) if has_exclude_adapters
137
123
 
138
124
  filtered_data
139
125
  end
140
126
 
127
+ # Build payload_rewrites: { adapter_name => { field => original_value } }
128
+ # Only fields with exclude_adapters.include?(adapter) get original value.
129
+ def build_payload_rewrites(event_data, pii_config)
130
+ adapters = AdapterResolver.resolve(event_data)
131
+ return {} unless adapters.any?
132
+
133
+ original_payload = event_data[:payload] || {}
134
+ rewrites = {}
135
+
136
+ adapters.each do |adapter_name|
137
+ adapter_rewrites = {}
138
+ pii_config[:fields]&.each do |field, opts|
139
+ next unless opts[:exclude_adapters]&.include?(adapter_name)
140
+
141
+ key = original_payload.key?(field) ? field : field.to_s
142
+ adapter_rewrites[key] = original_payload[key] if original_payload.key?(key)
143
+ end
144
+ rewrites[adapter_name] = adapter_rewrites if adapter_rewrites.any?
145
+ end
146
+ rewrites
147
+ end
148
+
141
149
  # Apply field-level filtering strategies
142
150
  #
143
151
  # @param payload [Hash] Payload to filter
144
152
  # @param config [Hash] PII configuration
153
+ # @param adapter_name [Symbol, nil] When set, use :skip for fields with exclude_adapters.include?(adapter_name)
145
154
  # @return [Hash] Filtered payload
146
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
147
- # Field strategies require case/when for each PII filtering strategy type
148
- def apply_field_strategies(payload, config)
155
+ # rubocop:disable Metrics/MethodLength
156
+ def apply_field_strategies(payload, config, adapter_name = nil)
149
157
  return payload unless config
150
158
 
151
159
  filtered = {}
152
160
 
153
161
  payload.each do |key, value|
154
- strategy = config.dig(:fields, key, :strategy) || :allow
162
+ normalized_key = key.is_a?(Symbol) ? key : key.to_sym
163
+ field_config = config.dig(:fields, normalized_key) || {}
164
+ strategy = field_config[:strategy] || :allow
165
+
166
+ # Per-adapter: use :skip for excluded adapters (e.g. audit gets original)
167
+ strategy = :allow if adapter_name && field_config[:exclude_adapters]&.include?(adapter_name)
155
168
 
156
169
  # rubocop:disable Lint/DuplicateBranch
157
- # Unknown strategies intentionally fallback to allow (same as :allow)
158
170
  filtered[key] = case strategy
159
171
  when :mask
160
172
  "[FILTERED]"
@@ -164,7 +176,7 @@ module E11y
164
176
  partial_mask(value)
165
177
  when :redact
166
178
  nil
167
- when :allow
179
+ when :allow, :skip
168
180
  value
169
181
  else
170
182
  value
@@ -174,34 +186,45 @@ module E11y
174
186
 
175
187
  filtered
176
188
  end
177
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
189
+ # rubocop:enable Metrics/MethodLength
178
190
 
179
191
  # Apply pattern-based filtering to string values
180
- #
181
- # @param data [Object] Data to filter (recursively)
182
- # @return [Object] Filtered data
183
- def apply_pattern_filtering(data)
192
+ def apply_pattern_filtering(data, pii_config = nil, path = [])
184
193
  case data
185
- when Hash
186
- data.transform_values { |v| apply_pattern_filtering(v) }
187
- when Array
188
- data.map { |v| apply_pattern_filtering(v) }
189
- when String
190
- filter_string_patterns(data)
191
- else
192
- data
194
+ when Hash then apply_pattern_filtering_hash(data, pii_config, path)
195
+ when Array then data.map { |v| apply_pattern_filtering(v, pii_config, path) }
196
+ when String then filter_string_if_needed(data, path, pii_config)
197
+ else data
193
198
  end
194
199
  end
195
200
 
196
- # Filter PII patterns in string
201
+ def apply_pattern_filtering_hash(data, pii_config, path)
202
+ data.each_with_object({}) do |(k, v), acc|
203
+ key_sym = k.is_a?(Symbol) ? k : k.to_sym
204
+ acc[k] = apply_pattern_filtering(v, pii_config, path + [key_sym])
205
+ end
206
+ end
207
+
208
+ def filter_string_if_needed(str, path, pii_config)
209
+ path_under_allowed_key?(path, pii_config) ? str : filter_string_patterns(str)
210
+ end
211
+
212
+ # Check if any ancestor key in path is explicitly allowed
213
+ def path_under_allowed_key?(path, pii_config)
214
+ return false unless pii_config && pii_config[:fields]
215
+
216
+ allowed_keys = pii_config[:fields].select { |_k, v| %i[allow skip].include?(v[:strategy]) }.keys
217
+ path.any? { |p| allowed_keys.include?(p) }
218
+ end
219
+
220
+ # Filter PII patterns in string (VALUE_PATTERNS only, not PASSWORD_FIELDS)
197
221
  #
198
222
  # @param str [String] String to filter
199
223
  # @return [String] Filtered string
200
224
  def filter_string_patterns(str)
201
225
  result = str.dup
202
226
 
203
- # Apply all PII patterns
204
- E11y::PII::Patterns::ALL.each do |pattern|
227
+ E11y::PII::Patterns::VALUE_PATTERNS.each do |pattern|
205
228
  result = result.gsub(pattern, "[FILTERED]")
206
229
  end
207
230
 
@@ -261,12 +284,18 @@ module E11y
261
284
  # Get Rails parameter filter
262
285
  #
263
286
  # Uses Rails.application.config.filter_parameters for PII filtering.
287
+ # When Rails is not loaded (e.g. unit tests), uses empty filter (no-op).
264
288
  #
265
289
  # @return [ActiveSupport::ParameterFilter] Parameter filter
266
290
  def parameter_filter
267
- @parameter_filter ||= ActiveSupport::ParameterFilter.new(
268
- Rails.application.config.filter_parameters
269
- )
291
+ return @parameter_filter if defined?(@parameter_filter) && !@parameter_filter.nil?
292
+
293
+ filters = if defined?(Rails) && Rails.application
294
+ Rails.application.config.filter_parameters
295
+ else
296
+ []
297
+ end
298
+ @parameter_filter = ActiveSupport::ParameterFilter.new(filters)
270
299
  end
271
300
  end
272
301
  # rubocop:enable Metrics/ClassLength
@@ -30,7 +30,7 @@ module E11y
30
30
  #
31
31
  # @example Critical Event Bypass (C02)
32
32
  # # Payment events bypass rate limiting → DLQ if limited
33
- # config.dlq_filter.always_save_patterns = [/^payment\./]
33
+ # config.dlq_filter.should_save?(event_data) # Event DSL: use_dlq
34
34
  #
35
35
  # # Result: Rate-limited payment events go to DLQ, not dropped
36
36
  #
@@ -40,22 +40,33 @@ module E11y
40
40
  # Initialize rate limiting middleware
41
41
  #
42
42
  # @param app [Object] Next middleware in pipeline
43
- # @param global_limit [Integer] Max events/sec globally (default: 10_000)
44
- # @param per_event_limit [Integer] Max events/sec per event type (default: 1_000)
45
- # @param window [Float] Time window in seconds (default: 1.0)
46
- def initialize(app, global_limit: 10_000, per_event_limit: 1_000, window: 1.0)
43
+ # @param global_limit [Integer] Max events/sec globally (default: from E11y.config)
44
+ # @param per_event_limit [Integer] Max events/sec per event type (default: from E11y.config)
45
+ # @param window [Float] Time window in seconds (default: from E11y.config)
46
+ def initialize(app, global_limit: nil, per_event_limit: nil, window: nil)
47
47
  super(app)
48
- @global_limit = global_limit
49
- @per_event_limit = per_event_limit
50
- @window = window
48
+ config = E11y.config
49
+ # When explicit limits are passed (e.g. from pipeline options), enable for this instance
50
+ explicit_opts = global_limit || per_event_limit || window
51
+ @enabled = explicit_opts ? true : config.rate_limiting_enabled
52
+ @global_limit = global_limit || config.rate_limiting_global_limit
53
+ @global_window = window || config.rate_limiting_global_window
54
+ @window = @global_window # Alias for spec compatibility
55
+ @per_event_limit = per_event_limit || config.rate_limiting_per_event_limit
56
+ @explicit_per_event = per_event_limit && window
51
57
 
52
58
  # Token buckets for rate limiting
53
- @global_bucket = TokenBucket.new(capacity: @global_limit, refill_rate: @global_limit, window: @window)
59
+ @global_bucket = TokenBucket.new(
60
+ capacity: @global_limit,
61
+ refill_rate: @global_limit,
62
+ window: @global_window
63
+ )
54
64
  @per_event_buckets = Hash.new do |hash, event_name|
65
+ limit_cfg = @explicit_per_event ? { limit: @per_event_limit, window: @window } : config.rate_limit_for(event_name)
55
66
  hash[event_name] = TokenBucket.new(
56
- capacity: @per_event_limit,
57
- refill_rate: @per_event_limit,
58
- window: @window
67
+ capacity: limit_cfg[:limit],
68
+ refill_rate: limit_cfg[:limit],
69
+ window: limit_cfg[:window]
59
70
  )
60
71
  end
61
72
 
@@ -67,6 +78,8 @@ module E11y
67
78
  # @param event_data [Hash] Event payload
68
79
  # @return [Hash, nil] Event data if allowed, nil if rate limited
69
80
  def call(event_data)
81
+ return @app.call(event_data) unless @enabled
82
+
70
83
  event_name = event_data[:event_name]
71
84
 
72
85
  # Check global rate limit
@@ -83,7 +96,7 @@ module E11y
83
96
  end
84
97
 
85
98
  # Rate limit not exceeded - continue pipeline
86
- event_data
99
+ @app.call(event_data)
87
100
  end
88
101
 
89
102
  private
@@ -97,16 +110,31 @@ module E11y
97
110
  def handle_rate_limited(event_data, limit_type)
98
111
  event_name = event_data[:event_name]
99
112
 
100
- # Log rate limiting
101
- warn "[E11y] Rate limit exceeded (#{limit_type}) for event: #{event_name}"
113
+ # Log rate limiting (via E11y.logger so it respects Rails.logger in test env)
114
+ E11y.logger&.warn("[E11y] Rate limit exceeded (#{limit_type}) for event: #{event_name}")
102
115
 
103
116
  # C02 Resolution: Check if event should be saved to DLQ
104
- return unless should_save_to_dlq?(event_data)
105
-
106
- save_to_dlq(event_data, limit_type)
117
+ if should_save_to_dlq?(event_data)
118
+ record_dropped_metric(event_data, "rate_limited_#{limit_type}_dlq")
119
+ save_to_dlq(event_data, limit_type)
120
+ else
121
+ record_dropped_metric(event_data, "rate_limited_#{limit_type}")
122
+ end
123
+ end
107
124
 
108
- # Non-critical events are dropped (no DLQ)
109
- # TODO: Track metric e11y.rate_limiter.dropped
125
+ # Record e11y_events_dropped_total metric (non-fatal, safe when Metrics unavailable)
126
+ #
127
+ # @param event_data [Hash] Event payload
128
+ # @param reason [String] Drop reason (e.g., sampled_out, rate_limited_global)
129
+ def record_dropped_metric(event_data, reason)
130
+ return unless defined?(E11y::Metrics) && E11y::Metrics.respond_to?(:increment)
131
+
132
+ E11y::Metrics.increment(:e11y_events_dropped_total, {
133
+ reason: reason,
134
+ event_type: event_data[:event_name].to_s
135
+ })
136
+ rescue StandardError
137
+ # non-fatal
110
138
  end
111
139
 
112
140
  # Check if rate-limited event should be saved to DLQ (C02 Resolution)
@@ -120,9 +148,8 @@ module E11y
120
148
  dlq_filter = E11y.config.dlq_filter
121
149
  return false unless dlq_filter
122
150
 
123
- # Check if event matches always_save_patterns
124
- event_name = event_data[:event_name]
125
- dlq_filter.always_save_patterns&.any? { |pattern| pattern.match?(event_name) }
151
+ # Use DLQ filter (Event DSL: use_dlq, severity, default)
152
+ dlq_filter.should_save?(event_data)
126
153
  end
127
154
 
128
155
  # Save rate-limited critical event to DLQ (C02 Resolution)
@@ -135,19 +162,19 @@ module E11y
135
162
  dlq_storage = E11y.config.dlq_storage
136
163
  return unless dlq_storage
137
164
 
165
+ per_event_limit = limit_type == :per_event ? E11y.config.rate_limit_for(event_data[:event_name])[:limit] : @per_event_limit
138
166
  dlq_storage.save(event_data, metadata: {
139
167
  reason: "rate_limited_#{limit_type}",
140
168
  limit_type: limit_type,
141
169
  global_limit: @global_limit,
142
- per_event_limit: @per_event_limit,
170
+ per_event_limit: per_event_limit,
143
171
  timestamp: Time.now.utc.iso8601
144
172
  })
145
173
 
146
- warn "[E11y] Rate-limited critical event saved to DLQ: #{event_data[:event_name]}"
147
- # TODO: Track metric e11y.rate_limiter.dlq_saved
174
+ E11y.logger&.warn("[E11y] Rate-limited critical event saved to DLQ: #{event_data[:event_name]}")
148
175
  rescue StandardError => e
149
176
  # Don't fail if DLQ save fails (C18 Resolution)
150
- warn "[E11y] Failed to save rate-limited event to DLQ: #{e.message}"
177
+ E11y.logger&.warn("[E11y] Failed to save rate-limited event to DLQ: #{e.message}")
151
178
  end
152
179
 
153
180
  # Token Bucket implementation for rate limiting