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
@@ -0,0 +1,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ # Thread-safe auto-populating registry for discovering and inspecting all defined E11y event classes.
5
+ #
6
+ # Events are registered automatically when `event_name` is set on a subclass of `E11y::Event::Base`.
7
+ # The registry is always-on (no configuration needed) and is safe for concurrent use.
8
+ #
9
+ # @example Discover all events
10
+ # E11y::Registry.event_classes
11
+ # # => [Events::OrderCreated, Events::PaymentFailed, ...]
12
+ #
13
+ # @example Find an event class by name
14
+ # E11y::Registry.find("order.created")
15
+ # # => Events::OrderCreated
16
+ #
17
+ # @example Filter events by severity
18
+ # E11y::Registry.where(severity: :error)
19
+ # # => [Events::PaymentFailed, ...]
20
+ #
21
+ # @example Generate documentation
22
+ # E11y::Registry.to_documentation
23
+ # # => [{ name: "order.created", class: "Events::OrderCreated", ... }, ...]
24
+ #
25
+ # @see UC-022 Event Registry
26
+ class Registry
27
+ class << self
28
+ # Singleton instance
29
+ #
30
+ # @return [Registry]
31
+ def instance
32
+ @instance ||= new
33
+ end
34
+
35
+ # Register an event class. Delegates to singleton instance.
36
+ #
37
+ # @param event_class [Class] Event class to register
38
+ # @return [void]
39
+ def register(event_class)
40
+ instance.register(event_class)
41
+ end
42
+
43
+ # Find event class by name. Delegates to singleton instance.
44
+ #
45
+ # @param event_name [String] Event name
46
+ # @param version [Integer, nil] Specific version (nil = latest)
47
+ # @return [Class, nil] Event class or nil
48
+ def find(event_name, version: nil)
49
+ instance.find(event_name, version: version)
50
+ end
51
+
52
+ # Return all registered event classes. Delegates to singleton instance.
53
+ #
54
+ # @return [Array<Class>]
55
+ def event_classes
56
+ instance.event_classes
57
+ end
58
+
59
+ # Filter events by criteria. Delegates to singleton instance.
60
+ #
61
+ # @param criteria [Hash] Filter criteria (:severity, :version, :adapter)
62
+ # @return [Array<Class>]
63
+ def where(**criteria)
64
+ instance.where(**criteria)
65
+ end
66
+
67
+ # Validate that an event is properly configured. Delegates to singleton instance.
68
+ #
69
+ # @param event_name [String]
70
+ # @return [Boolean]
71
+ def validate(event_name)
72
+ instance.validate(event_name)
73
+ end
74
+
75
+ # Clear all registered events. Delegates to singleton instance.
76
+ #
77
+ # @return [void]
78
+ def clear!
79
+ instance.clear!
80
+ end
81
+
82
+ # Number of unique event names registered. Delegates to singleton instance.
83
+ #
84
+ # @return [Integer]
85
+ def size
86
+ instance.size
87
+ end
88
+
89
+ # Generate documentation hash for all events. Delegates to singleton instance.
90
+ #
91
+ # @return [Array<Hash>]
92
+ def to_documentation
93
+ instance.to_documentation
94
+ end
95
+
96
+ # Return all versions of an event (ADR-012 §6.2).
97
+ #
98
+ # @param event_name [String] Event name
99
+ # @return [Array<Hash>] [{ version: N, class: Klass }, ...] sorted by version
100
+ def all_versions(event_name)
101
+ instance.all_versions(event_name)
102
+ end
103
+
104
+ # Return event names that have multiple versions (ADR-012 §6.2).
105
+ #
106
+ # @return [Array<String>]
107
+ def versioned_events
108
+ instance.versioned_events
109
+ end
110
+
111
+ # Reset the singleton instance (primarily for test isolation).
112
+ #
113
+ # After calling this, the next call to `.instance` creates a fresh registry.
114
+ # Note: previously registered events will NOT be re-registered unless their
115
+ # class definitions are re-evaluated.
116
+ #
117
+ # @return [void]
118
+ # @api private
119
+ def reset!
120
+ @instance = nil
121
+ end
122
+ end
123
+
124
+ # Initialize a new Registry instance.
125
+ #
126
+ # Creates an empty, thread-safe registry backed by a Mutex-protected Hash.
127
+ def initialize
128
+ @registry = {} # event_name (String) => Array<Class>
129
+ @mutex = Mutex.new
130
+ end
131
+
132
+ # Register an event class.
133
+ #
134
+ # Safe to call multiple times with the same class — idempotent.
135
+ # Silently ignores classes that do not respond to `event_name`
136
+ # or return a blank name (e.g. intermediate abstract classes).
137
+ #
138
+ # @param event_class [Class] Event class to register
139
+ # @return [void]
140
+ def register(event_class)
141
+ return unless event_class.respond_to?(:event_name)
142
+
143
+ name = begin
144
+ event_class.event_name
145
+ rescue StandardError
146
+ nil
147
+ end
148
+
149
+ return if name.nil? || name.empty? || name == "AnonymousEvent"
150
+
151
+ @mutex.synchronize do
152
+ @registry[name] ||= []
153
+ @registry[name] << event_class unless @registry[name].include?(event_class)
154
+ end
155
+ end
156
+
157
+ # Find event class by name.
158
+ #
159
+ # When multiple classes share the same event name (versioning), returns the
160
+ # latest-registered one by default. Pass `version:` to find a specific version.
161
+ #
162
+ # @param event_name [String, Symbol] Event name to look up
163
+ # @param version [Integer, nil] Specific version number (nil = latest)
164
+ # @return [Class, nil] Matching event class or nil
165
+ def find(event_name, version: nil)
166
+ entries = @mutex.synchronize { @registry[event_name.to_s]&.dup }
167
+ return nil if entries.nil? || entries.empty?
168
+
169
+ if version
170
+ entries.find { |klass| klass.respond_to?(:version) && klass.version == version }
171
+ else
172
+ entries.last # latest registered = latest version
173
+ end
174
+ end
175
+
176
+ # Return all registered event classes as a flat array.
177
+ #
178
+ # The returned array is a copy — mutating it does not affect the registry.
179
+ #
180
+ # @return [Array<Class>]
181
+ def event_classes
182
+ @mutex.synchronize { @registry.values.flatten.dup }
183
+ end
184
+
185
+ # Filter registered events by criteria.
186
+ #
187
+ # Supported criteria keys:
188
+ # - `:severity` — matches `klass.default_severity` or `klass.severity`
189
+ # - `:version` — matches `klass.version`
190
+ # - `:adapter` — matches if `klass.adapters` includes the value
191
+ #
192
+ # Unknown criteria keys always produce no matches (conservative).
193
+ #
194
+ # @param criteria [Hash]
195
+ # @return [Array<Class>]
196
+ def where(**criteria)
197
+ event_classes.select do |klass|
198
+ criteria.all? do |key, value|
199
+ case key
200
+ when :severity
201
+ # Support both default_severity and severity readers
202
+ reader = klass.respond_to?(:default_severity) ? :default_severity : :severity
203
+ klass.respond_to?(reader) && klass.public_send(reader) == value
204
+ when :version
205
+ klass.respond_to?(:version) && klass.version == value
206
+ when :adapter
207
+ klass.respond_to?(:adapters) && Array(klass.adapters).include?(value)
208
+ else
209
+ false
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ # Validate that a registered event has a compiled schema.
216
+ #
217
+ # Returns `false` for unknown events.
218
+ # Returns `true` if the class is registered and has a non-nil `compiled_schema`.
219
+ #
220
+ # @param event_name [String]
221
+ # @return [Boolean]
222
+ # rubocop:disable Naming/PredicateMethod -- "validate" is the established API name
223
+ def validate(event_name)
224
+ klass = find(event_name)
225
+ return false unless klass
226
+
227
+ klass.respond_to?(:compiled_schema) && !klass.compiled_schema.nil?
228
+ end
229
+ # rubocop:enable Naming/PredicateMethod
230
+
231
+ # Remove all entries from the registry.
232
+ #
233
+ # Primarily used in tests to avoid cross-test pollution.
234
+ #
235
+ # @return [void]
236
+ def clear!
237
+ @mutex.synchronize { @registry.clear }
238
+ end
239
+
240
+ # Number of unique event names in the registry.
241
+ #
242
+ # Note: multiple versions of the same event name count as 1.
243
+ #
244
+ # @return [Integer]
245
+ def size
246
+ @mutex.synchronize { @registry.size }
247
+ end
248
+
249
+ # Return all versions of an event (ADR-012 §6.2).
250
+ #
251
+ # @param event_name [String] Event name
252
+ # @return [Array<Hash>] [{ version: N, class: Klass }, ...] sorted by version
253
+ def all_versions(event_name)
254
+ entries = @mutex.synchronize { @registry[event_name.to_s]&.dup }
255
+ return [] if entries.nil? || entries.empty?
256
+
257
+ entries
258
+ .map { |klass| { version: klass.respond_to?(:version) ? klass.version : 1, class: klass } }
259
+ .sort_by { |h| h[:version] }
260
+ end
261
+
262
+ # Return event names that have multiple versions (ADR-012 §6.2).
263
+ #
264
+ # @return [Array<String>]
265
+ def versioned_events
266
+ @mutex.synchronize do
267
+ @registry.select { |_name, entries| entries.size >= 2 }.keys
268
+ end
269
+ end
270
+
271
+ # Generate a documentation-friendly hash for every registered event class.
272
+ #
273
+ # @return [Array<Hash>] Each entry contains `:name`, `:class`, `:version`,
274
+ # `:severity`, and `:schema_keys` (absent when not applicable).
275
+ def to_documentation
276
+ event_classes.map do |klass|
277
+ {
278
+ name: klass.respond_to?(:event_name) ? klass.event_name : klass.name,
279
+ class: klass.name,
280
+ version: klass.respond_to?(:version) ? klass.version : nil,
281
+ severity: klass.respond_to?(:severity) ? klass.severity : nil,
282
+ schema_keys: extract_schema_keys(klass)
283
+ }.compact
284
+ end
285
+ end
286
+
287
+ private
288
+
289
+ # Extract schema key names from an event class.
290
+ #
291
+ # Uses `compiled_schema.key_map` when available, falling back gracefully.
292
+ #
293
+ # @param klass [Class] Event class
294
+ # @return [Array<String>, nil]
295
+ def extract_schema_keys(klass)
296
+ return nil unless klass.respond_to?(:compiled_schema)
297
+
298
+ schema = klass.compiled_schema
299
+ return nil unless schema.respond_to?(:key_map)
300
+
301
+ schema.key_map.keys.map(&:name)
302
+ rescue StandardError
303
+ nil
304
+ end
305
+ end
306
+ end
@@ -115,7 +115,7 @@ module E11y
115
115
 
116
116
  # Handle OPEN state (fast fail).
117
117
  def handle_open_circuit
118
- increment_metric("e11y.circuit_breaker.rejected")
118
+ E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "rejected")
119
119
 
120
120
  raise CircuitOpenError, "Circuit breaker open for #{@adapter_name} " \
121
121
  "(opened at #{@opened_at}, timeout: #{@timeout_seconds}s)"
@@ -137,12 +137,10 @@ module E11y
137
137
  @failure_count = 0
138
138
  @success_count += 1
139
139
  end
140
-
141
- increment_metric("e11y.circuit_breaker.success")
142
140
  end
143
141
 
144
142
  # Handle failed execution in CLOSED state.
145
- def on_failure(error)
143
+ def on_failure(_error)
146
144
  @mutex.synchronize do
147
145
  @failure_count += 1
148
146
  @last_failure_time = Time.now
@@ -150,8 +148,6 @@ module E11y
150
148
  # Transition CLOSED → OPEN if threshold exceeded
151
149
  transition_to_open if @failure_count >= @failure_threshold
152
150
  end
153
-
154
- increment_metric("e11y.circuit_breaker.failure", error: error.class.name)
155
151
  end
156
152
 
157
153
  # Handle successful execution in HALF_OPEN state.
@@ -162,18 +158,14 @@ module E11y
162
158
  # Transition HALF_OPEN → CLOSED after enough successes
163
159
  transition_to_closed if @success_count >= @half_open_attempts
164
160
  end
165
-
166
- increment_metric("e11y.circuit_breaker.half_open_success")
167
161
  end
168
162
 
169
163
  # Handle failed execution in HALF_OPEN state.
170
- def on_half_open_failure(error)
164
+ def on_half_open_failure(_error)
171
165
  @mutex.synchronize do
172
166
  # Single failure in HALF_OPEN → back to OPEN
173
167
  transition_to_open
174
168
  end
175
-
176
- increment_metric("e11y.circuit_breaker.half_open_failure", error: error.class.name)
177
169
  end
178
170
 
179
171
  # Transition to OPEN state.
@@ -183,7 +175,8 @@ module E11y
183
175
  @failure_count = 0 # Reset for next cycle
184
176
  @success_count = 0
185
177
 
186
- increment_metric("e11y.circuit_breaker.opened")
178
+ E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "opened")
179
+ track_circuit_state_gauge
187
180
  end
188
181
 
189
182
  # Transition to HALF_OPEN state.
@@ -191,7 +184,8 @@ module E11y
191
184
  @state = STATE_HALF_OPEN
192
185
  @success_count = 0 # Reset success counter for testing
193
186
 
194
- increment_metric("e11y.circuit_breaker.half_opened")
187
+ E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "half_opened")
188
+ track_circuit_state_gauge
195
189
  end
196
190
 
197
191
  # Transition to CLOSED state.
@@ -202,16 +196,20 @@ module E11y
202
196
  @opened_at = nil
203
197
  @last_failure_time = nil
204
198
 
205
- increment_metric("e11y.circuit_breaker.closed")
199
+ E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "closed")
200
+ track_circuit_state_gauge
206
201
  end
207
202
 
208
- # Increment circuit breaker metric.
209
- #
210
- # @param metric_name [String] Metric name
211
- # @param tags [Hash] Additional tags
212
- def increment_metric(metric_name, tags = {})
213
- # TODO: Integrate with Yabeda metrics
214
- # E11y::Metrics.increment(metric_name, tags.merge(adapter: @adapter_name, state: @state))
203
+ # Track circuit breaker state gauge via ReliabilityMonitor.
204
+ def track_circuit_state_gauge
205
+ return unless defined?(E11y::SelfMonitoring::ReliabilityMonitor)
206
+
207
+ E11y::SelfMonitoring::ReliabilityMonitor.track_circuit_state(
208
+ adapter_name: @adapter_name,
209
+ state: @state.to_s
210
+ )
211
+ rescue StandardError => e
212
+ E11y.logger&.warn("E11y CircuitBreaker gauge error: #{e.message}")
215
213
  end
216
214
  end
217
215
  # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Reliability
5
+ module DLQ
6
+ # Abstract base class for Dead Letter Queue storage backends.
7
+ #
8
+ # Subclass this to implement a custom DLQ backend (file, Redis, database, etc.).
9
+ # All methods raise NotImplementedError by default except replay_batch (which
10
+ # delegates to replay).
11
+ #
12
+ # @see DLQ::FileAdapter for the file-based implementation
13
+ class Base
14
+ # Save a failed event to the DLQ.
15
+ #
16
+ # @param event_data [Hash] Event data
17
+ # @param metadata [Hash] Failure metadata
18
+ # @return [String] event ID
19
+ def save(event_data, metadata: {})
20
+ raise NotImplementedError, "#{self.class}#save is not implemented"
21
+ end
22
+
23
+ # List DLQ entries.
24
+ #
25
+ # @param limit [Integer]
26
+ # @param offset [Integer]
27
+ # @param filters [Hash]
28
+ # @return [Array<Hash>]
29
+ def list(limit: 100, offset: 0, filters: {})
30
+ raise NotImplementedError, "#{self.class}#list is not implemented"
31
+ end
32
+
33
+ # Return DLQ statistics.
34
+ #
35
+ # @return [Hash]
36
+ def stats
37
+ raise NotImplementedError, "#{self.class}#stats is not implemented"
38
+ end
39
+
40
+ # Replay a single event.
41
+ #
42
+ # @param event_id [String]
43
+ # @return [Boolean]
44
+ def replay(event_id)
45
+ raise NotImplementedError, "#{self.class}#replay is not implemented"
46
+ end
47
+
48
+ # Replay a batch of events. Delegates to replay for each ID.
49
+ #
50
+ # @param event_ids [Array<String>]
51
+ # @return [Hash] { success_count: Integer, failure_count: Integer }
52
+ def replay_batch(event_ids)
53
+ success_count = 0
54
+ failure_count = 0
55
+ event_ids.each do |id|
56
+ replay(id) ? success_count += 1 : failure_count += 1
57
+ end
58
+ { success_count: success_count, failure_count: failure_count }
59
+ end
60
+
61
+ # Delete an entry from the DLQ.
62
+ #
63
+ # @param event_id [String]
64
+ # @return [Boolean]
65
+ def delete(event_id)
66
+ raise NotImplementedError, "#{self.class}#delete is not implemented"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end