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,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "securerandom"
6
+ require_relative "base"
7
+
8
+ module E11y
9
+ module Reliability
10
+ module DLQ
11
+ # File-based Dead Letter Queue storage.
12
+ #
13
+ # Stores failed events to a JSONL file for later analysis/replay.
14
+ # Each line is a JSON object representing a failed event with metadata.
15
+ #
16
+ # @example Usage
17
+ # dlq = FileAdapter.new(file_path: "log/e11y_dlq.jsonl")
18
+ # dlq.save(event_data, metadata: { error: "Timeout", retry_count: 3 })
19
+ #
20
+ # @see ADR-013 §4 (Dead Letter Queue)
21
+ # @see UC-021 §3 (DLQ File Storage)
22
+ # rubocop:disable Metrics/ClassLength
23
+ # DLQ file storage is a cohesive unit handling event persistence, rotation, and querying
24
+ class FileAdapter < Base
25
+ # @param file_path [String] Path to DLQ file (default: log/e11y_dlq.jsonl)
26
+ # @param max_file_size_mb [Integer] Maximum file size in MB before rotation (default: 100)
27
+ # @param retention_days [Integer] Days to retain DLQ files (default: 30)
28
+ def initialize(file_path: nil, max_file_size_mb: 100, retention_days: 30)
29
+ super()
30
+ @file_path = file_path || default_file_path
31
+ @max_file_size_bytes = max_file_size_mb * 1024 * 1024
32
+ @retention_days = retention_days
33
+ @mutex = Mutex.new
34
+
35
+ ensure_directory_exists
36
+ end
37
+
38
+ # Save failed event to DLQ.
39
+ #
40
+ # @param event_data [Hash] Event data
41
+ # @param metadata [Hash] Failure metadata (error, retry_count, adapter, etc.)
42
+ # @return [String] Event ID (UUID)
43
+ # DLQ save requires building entry, writing, rotation, cleanup, and metrics
44
+ def save(event_data, metadata: {})
45
+ event_id = SecureRandom.uuid
46
+ timestamp = Time.now.utc
47
+
48
+ dlq_entry = {
49
+ id: event_id,
50
+ timestamp: timestamp.iso8601(3),
51
+ event_name: event_data[:event_name],
52
+ event_data: event_data,
53
+ metadata: metadata.merge(
54
+ failed_at: timestamp.iso8601(3),
55
+ retry_count: metadata[:retry_count] || 0,
56
+ error_message: metadata[:error]&.message,
57
+ error_class: metadata[:error]&.class&.name
58
+ )
59
+ }
60
+
61
+ write_entry(dlq_entry)
62
+ rotate_if_needed
63
+ cleanup_old_files
64
+
65
+ increment_metric("e11y.dlq.saved", event_name: event_data[:event_name])
66
+
67
+ event_id
68
+ end
69
+
70
+ # List DLQ entries with optional filters.
71
+ #
72
+ # @param limit [Integer] Maximum entries to return
73
+ # @param offset [Integer] Number of entries to skip
74
+ # @param filters [Hash] Filter options (event_name, after, before)
75
+ # @return [Array<Hash>] Array of DLQ entries
76
+ # rubocop:disable Metrics/AbcSize
77
+ # DLQ listing requires file iteration, pagination, multiple filters, and error handling
78
+ def list(limit: 100, offset: 0, filters: {})
79
+ entries = []
80
+
81
+ return entries unless File.exist?(@file_path)
82
+
83
+ File.foreach(@file_path).with_index do |line, index|
84
+ next if index < offset
85
+ break if entries.size >= limit
86
+
87
+ entry = JSON.parse(line, symbolize_names: true)
88
+
89
+ # Apply filters
90
+ next if filters[:event_name] && entry[:event_name] != filters[:event_name]
91
+ next if filters[:after] && Time.parse(entry[:timestamp]) < filters[:after]
92
+ next if filters[:before] && Time.parse(entry[:timestamp]) > filters[:before]
93
+
94
+ entries << entry
95
+ end
96
+
97
+ entries
98
+ rescue JSON::ParserError => e
99
+ # Log parsing error but don't crash
100
+ increment_metric("e11y.dlq.parse_error", error: e.class.name)
101
+ entries
102
+ end
103
+ # rubocop:enable Metrics/AbcSize
104
+
105
+ # Get DLQ statistics.
106
+ #
107
+ # @return [Hash] Statistics (total_entries, file_size_mb, oldest_entry, newest_entry)
108
+ # rubocop:disable Metrics/AbcSize
109
+ # DLQ stats requires reading file size, counting entries, extracting timestamps, and error handling
110
+ def stats
111
+ return default_stats unless File.exist?(@file_path)
112
+
113
+ file_size_bytes = File.size(@file_path)
114
+ total_entries = File.foreach(@file_path).count
115
+
116
+ oldest_entry = nil
117
+ newest_entry = nil
118
+
119
+ # Read first and last line for oldest/newest timestamps
120
+ File.foreach(@file_path).with_index do |line, index|
121
+ entry = JSON.parse(line, symbolize_names: true)
122
+ oldest_entry = entry[:timestamp] if index.zero?
123
+ newest_entry = entry[:timestamp]
124
+ end
125
+
126
+ {
127
+ total_entries: total_entries,
128
+ file_size_mb: (file_size_bytes / 1024.0 / 1024.0).round(2),
129
+ oldest_entry: oldest_entry,
130
+ newest_entry: newest_entry,
131
+ file_path: @file_path
132
+ }
133
+ rescue StandardError => e
134
+ increment_metric("e11y.dlq.stats_error", error: e.class.name)
135
+ default_stats
136
+ end
137
+ # rubocop:enable Metrics/AbcSize
138
+
139
+ # Replay single event from DLQ.
140
+ #
141
+ # Re-dispatches the stored event_data to all registered adapters so the
142
+ # event reaches every adapter regardless of original routing configuration.
143
+ #
144
+ # NOTE: Delivers directly to adapters, bypassing the middleware pipeline.
145
+ # This means middleware (PII filtering, rate limiting, routing) is NOT applied.
146
+ # Trade-off: ensures event reaches all registered adapters regardless of routing rules.
147
+ #
148
+ # @param event_id [String] Event ID to replay
149
+ # @return [Boolean] true if replayed successfully
150
+ def replay(event_id)
151
+ entry = find_entry(event_id)
152
+ return false unless entry
153
+
154
+ # Reconstruct event_data from stored entry (keys are symbolized after JSON parse)
155
+ event_data = entry[:event_data]
156
+
157
+ # Deliver directly to all registered adapters, bypassing routing.
158
+ # This ensures the replayed event reaches every adapter (including
159
+ # adapters added after the original event was stored in the DLQ).
160
+ E11y.configuration.adapters.each_value do |adapter|
161
+ adapter.write(event_data)
162
+ rescue StandardError => e
163
+ E11y.logger.error("DLQ replay write failed: #{e.message}")
164
+ end
165
+
166
+ increment_metric("e11y.dlq.replayed", event_name: entry[:event_name])
167
+ true
168
+ rescue StandardError => e
169
+ increment_metric("e11y.dlq.replay_failed", error: e.class.name)
170
+ false
171
+ end
172
+
173
+ # Delete entry from DLQ.
174
+ #
175
+ # Rewrites the JSONL file excluding the line whose :id matches event_id.
176
+ # Returns true if an entry was found and removed, false otherwise.
177
+ #
178
+ # @param event_id [String] Event ID to delete
179
+ # @return [Boolean] true if deleted
180
+ # delete is an action method returning boolean status, not a predicate query
181
+ # rubocop:disable Metrics/AbcSize -- file rewrite with mutex, filters, error handling
182
+ def delete(event_id)
183
+ return false unless File.exist?(@file_path)
184
+
185
+ deleted = false
186
+ @mutex.synchronize do
187
+ all_lines = File.readlines(@file_path).map(&:chomp).reject(&:empty?)
188
+ original_count = all_lines.length
189
+
190
+ remaining = all_lines.reject do |line|
191
+ entry = JSON.parse(line, symbolize_names: true)
192
+ entry[:id].to_s == event_id.to_s
193
+ end
194
+
195
+ if remaining.length < original_count
196
+ content = remaining.join("\n")
197
+ content += "\n" unless content.empty? || content.end_with?("\n")
198
+ File.write(@file_path, content)
199
+ deleted = true
200
+ end
201
+ end
202
+ deleted
203
+ rescue StandardError => e
204
+ E11y.logger.error("DLQ delete failed for #{event_id}: #{e.message}")
205
+ false
206
+ end
207
+ # rubocop:enable Metrics/AbcSize
208
+
209
+ private
210
+
211
+ # Get default file path (log/e11y_dlq.jsonl).
212
+ def default_file_path
213
+ ::Rails.root.join("log", "e11y_dlq.jsonl").to_s
214
+ end
215
+
216
+ # Ensure log directory exists.
217
+ def ensure_directory_exists
218
+ dir = File.dirname(@file_path)
219
+ FileUtils.mkdir_p(dir)
220
+ end
221
+
222
+ # Write DLQ entry to file (thread-safe).
223
+ def write_entry(entry)
224
+ @mutex.synchronize do
225
+ File.open(@file_path, "a") do |f|
226
+ f.flock(File::LOCK_EX)
227
+ f.puts(JSON.generate(entry))
228
+ end
229
+ end
230
+ end
231
+
232
+ # Rotate file if size exceeds max_file_size.
233
+ def rotate_if_needed
234
+ return unless File.exist?(@file_path)
235
+ return if File.size(@file_path) < @max_file_size_bytes
236
+
237
+ @mutex.synchronize do
238
+ # Rotate: log/e11y_dlq.jsonl → log/e11y_dlq.2026-01-20T12:34:56Z.jsonl
239
+ timestamp = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
240
+ rotated_path = @file_path.sub(/\.jsonl$/, ".#{timestamp}.jsonl")
241
+
242
+ FileUtils.mv(@file_path, rotated_path)
243
+
244
+ increment_metric("e11y.dlq.rotated", new_file: rotated_path)
245
+ end
246
+ end
247
+
248
+ # Cleanup old rotated files.
249
+ def cleanup_old_files
250
+ dir = File.dirname(@file_path)
251
+ base_name = File.basename(@file_path, ".jsonl")
252
+
253
+ # Find all rotated files: e11y_dlq.*.jsonl
254
+ pattern = File.join(dir, "#{base_name}.*.jsonl")
255
+
256
+ Dir.glob(pattern).each do |file|
257
+ next unless File.file?(file)
258
+
259
+ file_age_days = (Time.now - File.mtime(file)) / 86_400
260
+
261
+ if file_age_days > @retention_days
262
+ File.delete(file)
263
+ increment_metric("e11y.dlq.cleaned_up", file: file)
264
+ end
265
+ end
266
+ end
267
+
268
+ # Find DLQ entry by ID.
269
+ def find_entry(event_id)
270
+ return nil unless File.exist?(@file_path)
271
+
272
+ File.foreach(@file_path) do |line|
273
+ entry = JSON.parse(line, symbolize_names: true)
274
+ return entry if entry[:id] == event_id
275
+ end
276
+
277
+ nil
278
+ rescue JSON::ParserError
279
+ nil
280
+ end
281
+
282
+ # Default stats when file doesn't exist.
283
+ def default_stats
284
+ {
285
+ total_entries: 0,
286
+ file_size_mb: 0.0,
287
+ oldest_entry: nil,
288
+ newest_entry: nil,
289
+ file_path: @file_path
290
+ }
291
+ end
292
+
293
+ # Increment DLQ metric (no-op when E11y::Metrics backend not configured).
294
+ def increment_metric(metric_name, tags = {})
295
+ E11y::Metrics.increment(metric_name, **tags)
296
+ end
297
+ end
298
+ # rubocop:enable Metrics/ClassLength
299
+ end
300
+ end
301
+ end
@@ -38,7 +38,6 @@ module E11y
38
38
  # @param event_data [Hash] Event data
39
39
  # @param metadata [Hash] Failure metadata (error, retry_count, adapter, etc.)
40
40
  # @return [String] Event ID (UUID)
41
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
42
41
  # DLQ save requires building entry, writing, rotation, cleanup, and metrics
43
42
  def save(event_data, metadata: {})
44
43
  event_id = SecureRandom.uuid
@@ -61,11 +60,11 @@ module E11y
61
60
  rotate_if_needed
62
61
  cleanup_old_files
63
62
 
64
- increment_metric("e11y.dlq.saved", event_name: event_data[:event_name])
63
+ E11y::Metrics.increment("e11y.dlq.saved", event_name: event_data[:event_name])
64
+ update_dlq_size_gauge
65
65
 
66
66
  event_id
67
67
  end
68
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
69
68
 
70
69
  # List DLQ entries with optional filters.
71
70
  #
@@ -73,7 +72,7 @@ module E11y
73
72
  # @param offset [Integer] Number of entries to skip
74
73
  # @param filters [Hash] Filter options (event_name, after, before)
75
74
  # @return [Array<Hash>] Array of DLQ entries
76
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
75
+ # rubocop:disable Metrics/AbcSize
77
76
  # DLQ listing requires file iteration, pagination, multiple filters, and error handling
78
77
  def list(limit: 100, offset: 0, filters: {})
79
78
  entries = []
@@ -97,15 +96,14 @@ module E11y
97
96
  entries
98
97
  rescue JSON::ParserError => e
99
98
  # Log parsing error but don't crash
100
- increment_metric("e11y.dlq.parse_error", error: e.class.name)
99
+ E11y::Metrics.increment("e11y.dlq.parse_error", error: e.class.name)
101
100
  entries
102
101
  end
103
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
102
+ # rubocop:enable Metrics/AbcSize
104
103
 
105
104
  # Get DLQ statistics.
106
105
  #
107
106
  # @return [Hash] Statistics (total_entries, file_size_mb, oldest_entry, newest_entry)
108
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
109
107
  # DLQ stats requires reading file size, counting entries, extracting timestamps, and error handling
110
108
  def stats
111
109
  return default_stats unless File.exist?(@file_path)
@@ -130,29 +128,32 @@ module E11y
130
128
  newest_entry: newest_entry,
131
129
  file_path: @file_path
132
130
  }
133
- rescue StandardError => e
134
- increment_metric("e11y.dlq.stats_error", error: e.class.name)
131
+ rescue StandardError
135
132
  default_stats
136
133
  end
137
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
138
134
 
139
135
  # Replay single event from DLQ.
140
136
  #
137
+ # Re-dispatches event through E11y pipeline so it reaches adapters.
138
+ #
141
139
  # @param event_id [String] Event ID to replay
142
140
  # @return [Boolean] true if replayed successfully
143
141
  def replay(event_id)
144
142
  entry = find_entry(event_id)
145
143
  return false unless entry
146
144
 
147
- # Re-dispatch event through E11y pipeline
148
- # TODO: Implement E11y::Pipeline.dispatch
149
- # E11y::Pipeline.dispatch(entry[:event_data], metadata: entry[:metadata].merge(replayed: true))
145
+ event_data = entry[:event_data]
146
+ return false unless event_data
150
147
 
151
- # For now, just mark as replayed
152
- increment_metric("e11y.dlq.replayed", event_name: entry[:event_name])
148
+ # F-004/C07: Mark as DLQ-replayed so PIIFilter skips (avoid double-hashing)
149
+ event_data = event_data.dup
150
+ event_data[:dlq_replayed] = true
151
+
152
+ E11y.config.built_pipeline.call(event_data)
153
+ E11y::Metrics.increment("e11y.dlq.replayed", event_name: entry[:event_name])
153
154
  true
154
155
  rescue StandardError => e
155
- increment_metric("e11y.dlq.replay_failed", error: e.class.name)
156
+ E11y::Metrics.increment("e11y.dlq.replay_failed", error: e.class.name)
156
157
  false
157
158
  end
158
159
 
@@ -177,23 +178,58 @@ module E11y
177
178
 
178
179
  # Delete entry from DLQ.
179
180
  #
180
- # Note: This is a simplified implementation.
181
- # In production, consider using a database or append-only log with tombstones.
181
+ # Rewrites file excluding the entry. For large files this is expensive.
182
182
  #
183
183
  # @param event_id [String] Event ID to delete
184
184
  # @return [Boolean] true if deleted
185
- # rubocop:disable Naming/PredicateMethod
186
185
  # delete is an action method returning boolean status, not a predicate query
187
- def delete(_event_id)
188
- # TODO: Implement deletion (requires rewriting file)
189
- # For JSONL, deletion is expensive (requires full file rewrite)
190
- # Consider marking as deleted instead or using database
186
+ def delete(event_id)
187
+ return false unless File.exist?(@file_path)
188
+
189
+ entries, found = read_entries_excluding(event_id)
190
+ return false unless found
191
+
192
+ rewrite_file_with(entries)
193
+ update_dlq_size_gauge
194
+ true
195
+ rescue StandardError
191
196
  false
192
197
  end
193
- # rubocop:enable Naming/PredicateMethod
194
198
 
195
199
  private
196
200
 
201
+ def update_dlq_size_gauge
202
+ return unless defined?(E11y::Metrics) && E11y::Metrics.respond_to?(:gauge)
203
+
204
+ count = stats[:total_entries]
205
+ E11y::Metrics.gauge(:e11y_dlq_size, count)
206
+ rescue StandardError
207
+ # Non-fatal: gauge update must not break DLQ operations
208
+ end
209
+
210
+ def read_entries_excluding(event_id)
211
+ entries = []
212
+ found = false
213
+ File.foreach(@file_path) do |line|
214
+ entry = JSON.parse(line, symbolize_names: true)
215
+ if entry[:id] == event_id
216
+ found = true
217
+ else
218
+ entries << entry
219
+ end
220
+ end
221
+ [entries, found]
222
+ end
223
+
224
+ def rewrite_file_with(entries)
225
+ @mutex.synchronize do
226
+ File.open(@file_path, "w") do |f|
227
+ f.flock(File::LOCK_EX)
228
+ entries.each { |e| f.puts(JSON.generate(e)) }
229
+ end
230
+ end
231
+ end
232
+
197
233
  # Get default file path (log/e11y_dlq.jsonl).
198
234
  def default_file_path
199
235
  ::Rails.root.join("log", "e11y_dlq.jsonl").to_s
@@ -226,8 +262,6 @@ module E11y
226
262
  rotated_path = @file_path.sub(/\.jsonl$/, ".#{timestamp}.jsonl")
227
263
 
228
264
  FileUtils.mv(@file_path, rotated_path)
229
-
230
- increment_metric("e11y.dlq.rotated", new_file: rotated_path)
231
265
  end
232
266
  end
233
267
 
@@ -244,10 +278,7 @@ module E11y
244
278
 
245
279
  file_age_days = (Time.now - File.mtime(file)) / 86_400
246
280
 
247
- if file_age_days > @retention_days
248
- File.delete(file)
249
- increment_metric("e11y.dlq.cleaned_up", file: file)
250
- end
281
+ File.delete(file) if file_age_days > @retention_days
251
282
  end
252
283
  end
253
284
 
@@ -277,10 +308,8 @@ module E11y
277
308
  end
278
309
 
279
310
  # Increment DLQ metric.
280
- def increment_metric(metric_name, tags = {})
281
- # TODO: Integrate with Yabeda metrics
282
- # E11y::Metrics.increment(metric_name, tags)
283
- end
311
+ #
312
+ # Normalizes metric_name like "e11y.dlq.saved" to :e11y_dlq_saved_total.
284
313
  end
285
314
  # rubocop:enable Metrics/ClassLength
286
315
  end
@@ -5,91 +5,79 @@ module E11y
5
5
  module DLQ
6
6
  # DLQ Filter determines which failed events should be saved to DLQ.
7
7
  #
8
- # Supports:
9
- # - Always save patterns (e.g., payment.*, audit.*)
10
- # - Always discard patterns (e.g., debug.*, test.*)
11
- # - Severity-based filtering (e.g., always save :error, :fatal)
8
+ # Uses Event DSL (use_dlq) when event class is registered.
9
+ # Audit events (Presets::AuditEvent) have use_dlq true by default.
12
10
  #
13
- # @example Configuration
14
- # filter = Filter.new(
15
- # always_save_patterns: [/^payment\./, /^audit\./],
16
- # always_discard_patterns: [/^debug\./, /^test\./],
17
- # save_severities: [:error, :fatal]
18
- # )
11
+ # Priority order:
12
+ # 1. Event class use_dlq == false → discard
13
+ # 2. Event class use_dlq == true → save
14
+ # 3. Severity-based (save_severities)
15
+ # 4. Default behavior
19
16
  #
20
- # filter.should_save?(event_data) # => true/false
17
+ # @example Event DSL
18
+ # class Events::AuditLogin < E11y::Events::BaseAuditEvent
19
+ # # use_dlq true from preset
20
+ # end
21
+ #
22
+ # class Events::DebugTrace < E11y::Event::Base
23
+ # use_dlq false
24
+ # end
21
25
  #
22
26
  # @see ADR-013 §4.3 (DLQ Filter)
23
27
  # @see UC-021 §3.2 (DLQ Filter Configuration)
24
28
  class Filter
25
- # @param always_save_patterns [Array<Regexp>] Event patterns to always save
26
- # @param always_discard_patterns [Array<Regexp>] Event patterns to always discard
27
29
  # @param save_severities [Array<Symbol>] Severities to always save (:error, :fatal)
28
- # @param default_behavior [Symbol] Default behavior when no rule matches (:save or :discard)
30
+ # @param default_behavior [Symbol] Default when no Event DSL rule (:save or :discard)
29
31
  def initialize(
30
- always_save_patterns: [],
31
- always_discard_patterns: [],
32
32
  save_severities: %i[error fatal],
33
33
  default_behavior: :save
34
34
  )
35
- @always_save_patterns = always_save_patterns
36
- @always_discard_patterns = always_discard_patterns
37
35
  @save_severities = save_severities
38
36
  @default_behavior = default_behavior
39
37
  end
40
38
 
41
39
  # Check if event should be saved to DLQ.
42
40
  #
43
- # Priority order:
44
- # 1. Always discard patterns (highest priority)
45
- # 2. Always save patterns
46
- # 3. Severity-based rules
47
- # 4. Default behavior
48
- #
49
41
  # @param event_data [Hash] Event data
42
+ # @param error [StandardError, nil] The error that caused the DLQ save (optional)
50
43
  # @return [Boolean] true if event should be saved to DLQ
51
44
  # rubocop:disable Metrics/MethodLength
52
- # DLQ filter requires 4-priority decision tree with metrics tracking for each branch
53
- def should_save?(event_data)
54
- event_name = event_data[:event_name].to_s
45
+ def should_save?(event_data, _error = nil)
46
+ event_class = resolve_event_class(event_data[:event_name])
55
47
  severity = event_data[:severity]
56
48
 
57
- # Priority 1: Always discard (highest priority)
58
- if matches_patterns?(event_name, @always_discard_patterns)
59
- increment_metric("e11y.dlq.filter.discarded", reason: "always_discard_pattern")
49
+ # Priority 1: Event DSL use_dlq == false
50
+ if event_class.respond_to?(:use_dlq) && event_class.use_dlq == false
51
+ increment_filter_metric("discarded", "use_dlq")
60
52
  return false
61
53
  end
62
54
 
63
- # Priority 2: Always save
64
- if matches_patterns?(event_name, @always_save_patterns)
65
- increment_metric("e11y.dlq.filter.saved", reason: "always_save_pattern")
55
+ # Priority 2: Event DSL use_dlq == true
56
+ if event_class.respond_to?(:use_dlq) && event_class.use_dlq == true
57
+ increment_filter_metric("saved", "use_dlq")
66
58
  return true
67
59
  end
68
60
 
69
61
  # Priority 3: Severity-based
70
62
  if @save_severities.include?(severity)
71
- increment_metric("e11y.dlq.filter.saved", reason: "severity")
63
+ increment_filter_metric("saved", "severity")
72
64
  return true
73
65
  end
74
66
 
75
67
  # Priority 4: Default behavior
76
68
  if @default_behavior == :save
77
- increment_metric("e11y.dlq.filter.saved", reason: "default")
69
+ increment_filter_metric("saved", "default")
78
70
  true
79
71
  else
80
- increment_metric("e11y.dlq.filter.discarded", reason: "default")
72
+ increment_filter_metric("discarded", "default")
81
73
  false
82
74
  end
83
75
  end
84
76
  # rubocop:enable Metrics/MethodLength
85
77
 
86
- # Get filter statistics.
87
- #
88
78
  # @return [Hash] Filter configuration stats
89
79
  def stats
90
80
  {
91
- always_save_patterns: @always_save_patterns.map(&:inspect),
92
- always_discard_patterns: @always_discard_patterns.map(&:inspect),
93
81
  save_severities: @save_severities,
94
82
  default_behavior: @default_behavior
95
83
  }
@@ -97,22 +85,17 @@ module E11y
97
85
 
98
86
  private
99
87
 
100
- # Check if event name matches any of the patterns.
101
- #
102
- # @param event_name [String] Event name
103
- # @param patterns [Array<Regexp>] Patterns to match
104
- # @return [Boolean] true if event matches any pattern
105
- def matches_patterns?(event_name, patterns)
106
- patterns.any? { |pattern| pattern.match?(event_name) }
88
+ def resolve_event_class(event_name)
89
+ return nil unless event_name
90
+ return nil unless defined?(E11y::Registry) && E11y::Registry.respond_to?(:find)
91
+
92
+ E11y::Registry.find(event_name.to_s)
107
93
  end
108
94
 
109
- # Increment DLQ filter metric.
110
- #
111
- # @param metric_name [String] Metric name
112
- # @param tags [Hash] Additional tags
113
- def increment_metric(metric_name, tags = {})
114
- # TODO: Integrate with Yabeda metrics
115
- # E11y::Metrics.increment(metric_name, tags)
95
+ def increment_filter_metric(action, reason)
96
+ return unless defined?(E11y::Metrics) && E11y::Metrics.respond_to?(:increment)
97
+
98
+ E11y::Metrics.increment(:e11y_dlq_filter_decisions_total, { action: action, reason: reason })
116
99
  end
117
100
  end
118
101
  end