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,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+
6
+ module E11y
7
+ module Adapters
8
+ # Development-only adapter that stores events in a local JSONL file
9
+ # and exposes a rich read API for TUI, Browser Overlay, and MCP Server.
10
+ #
11
+ # Auto-registered by Railtie in development/test environments.
12
+ # Do not use in production.
13
+ #
14
+ # @example Manual setup
15
+ # adapter = E11y::Adapters::DevLog.new(
16
+ # path: Rails.root.join("log", "e11y_dev.jsonl"),
17
+ # max_size: 50.megabytes,
18
+ # keep_rotated: 5
19
+ # )
20
+ class DevLog < Base
21
+ # @param path [String, Pathname]
22
+ # @param max_size [Integer] Rotation threshold in bytes (default 50 MB)
23
+ # @param max_lines [Integer] Rotation threshold in line count (default 10_000)
24
+ # @param keep_rotated [Integer] Number of .N.gz files to retain (default 5)
25
+ # @param enable_watcher [Boolean] Reserved for future file-watcher integration
26
+ def initialize(path: "log/e11y_dev.jsonl",
27
+ max_size: FileStore::DEFAULT_MAX_SIZE,
28
+ max_lines: FileStore::DEFAULT_MAX_LINES,
29
+ keep_rotated: FileStore::DEFAULT_KEEP_ROTATED,
30
+ enable_watcher: false)
31
+ super({})
32
+ @store = FileStore.new(path: path, max_size: max_size,
33
+ max_lines: max_lines, keep_rotated: keep_rotated)
34
+ @query = Query.new(@store.path)
35
+ @enable_watcher = enable_watcher
36
+ end
37
+
38
+ # Write a single event to the JSONL file.
39
+ #
40
+ # @param event_data [Hash] Event from the E11y pipeline
41
+ # @return [Boolean] true on success, false on error
42
+ def write(event_data)
43
+ @store.append(serialize(event_data))
44
+ true
45
+ rescue StandardError => e
46
+ warn "[E11y::DevLog] write failed: #{e.message}"
47
+ false
48
+ end
49
+
50
+ # --- Read API (delegated to Query) ---
51
+
52
+ # @see Query#stored_events
53
+ def stored_events(limit: 1000, severity: nil, source: nil)
54
+ @query.stored_events(limit: limit, severity: severity, source: source)
55
+ end
56
+
57
+ # @see Query#find_event
58
+ def find_event(id) = @query.find_event(id)
59
+
60
+ # @see Query#search
61
+ def search(query_str, limit: 500) = @query.search(query_str, limit: limit)
62
+
63
+ # @see Query#events_by_name
64
+ def events_by_name(name, limit: 500)
65
+ @query.stored_events(limit: limit).select { |e| e["event_name"] == name }
66
+ end
67
+
68
+ # @see Query#events_by_severity
69
+ def events_by_severity(sev, limit: 500)
70
+ @query.stored_events(limit: limit, severity: sev)
71
+ end
72
+
73
+ # @see Query#events_by_trace
74
+ def events_by_trace(trace_id) = @query.events_by_trace(trace_id)
75
+
76
+ # @see Query#interactions
77
+ def interactions(window_ms: 500, limit: 50, source: nil)
78
+ @query.interactions(window_ms: window_ms, limit: limit, source: source)
79
+ end
80
+
81
+ # @see Query#stats
82
+ def stats = @query.stats
83
+
84
+ # @see Query#updated_since?
85
+ def updated_since?(timestamp) = @query.updated_since?(timestamp)
86
+
87
+ # @see Query#clear!
88
+ def clear! = @query.clear!
89
+
90
+ # Advertise dev_log and readable capabilities.
91
+ def capabilities
92
+ super.merge(dev_log: true, readable: true)
93
+ end
94
+
95
+ private
96
+
97
+ def serialize(event_data)
98
+ data = event_data.is_a?(::Hash) ? event_data.transform_keys(&:to_s) : {}
99
+ enrich_ids!(data)
100
+ enrich_metadata!(data)
101
+ ::JSON.generate(data)
102
+ end
103
+
104
+ def enrich_ids!(data)
105
+ data["id"] ||= ::SecureRandom.uuid
106
+ data["timestamp"] ||= ::Time.now.utc.iso8601(3)
107
+ end
108
+
109
+ def enrich_metadata!(data)
110
+ source = ::Thread.current[:e11y_source] || "web"
111
+ meta = (data["metadata"] || {}).dup
112
+ meta["source"] ||= source
113
+ meta["started_at"] ||= data["timestamp"]
114
+ data["metadata"] = meta
115
+ end
116
+ end
117
+ end
118
+ end
@@ -26,11 +26,8 @@ module E11y
26
26
  #
27
27
  # adapter.write(event_name: "user.login", severity: :info)
28
28
  #
29
- # @example With Registry
30
- # E11y::Adapters::Registry.register(
31
- # :file_logger,
32
- # E11y::Adapters::File.new(path: "log/events.log")
33
- # )
29
+ # @example Configuration
30
+ # config.adapters[:file] = E11y::Adapters::File.new(path: "log/events.log")
34
31
  # rubocop:disable Metrics/ClassLength
35
32
  # File adapter contains file rotation and buffering logic as cohesive unit
36
33
  class File < Base
@@ -152,7 +149,7 @@ module E11y
152
149
 
153
150
  # Open file for writing
154
151
  def open_file!
155
- @file = ::File.open(@path, "a")
152
+ @file = ::File.new(@path, "a")
156
153
  @file.sync = true
157
154
  @current_date = Date.today if @rotation == :daily
158
155
  end
@@ -39,6 +39,7 @@ module E11y
39
39
  # test_adapter = E11y::Adapters::InMemory.new(max_events: nil)
40
40
  #
41
41
  # @see ADR-004 §9.1 (In-Memory Test Adapter)
42
+ # rubocop:disable Metrics/ClassLength
42
43
  class InMemory < Base
43
44
  # Default maximum number of events to store
44
45
  DEFAULT_MAX_EVENTS = 1000
@@ -118,17 +119,28 @@ module E11y
118
119
  end
119
120
  end
120
121
 
122
+ alias clear clear!
123
+
121
124
  # Find events matching pattern
122
125
  #
123
- # @param pattern [String, Regexp] Pattern to match against event_name
126
+ # @param pattern [String, Regexp, Class] Event name pattern or event class
124
127
  # @return [Array<Hash>] Matching events
125
128
  #
126
129
  # @example
127
130
  # adapter.find_events(/order/) # All order.* events
128
131
  # adapter.find_events("order.paid") # Exact match
132
+ # adapter.find_events(Events::OrderPaid) # By event class
129
133
  def find_events(pattern)
130
- pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
131
- @events.select { |event| event[:event_name].to_s.match?(pattern) }
134
+ pattern = event_pattern_for(pattern)
135
+ @events.select { |event| event_matches?(event, pattern) }
136
+ end
137
+
138
+ # Find first event matching pattern
139
+ #
140
+ # @param pattern [String, Regexp, Class] Event name pattern or event class
141
+ # @return [Hash, nil] First matching event or nil
142
+ def find_event(pattern)
143
+ find_events(pattern).first
132
144
  end
133
145
 
134
146
  # Count events by name
@@ -138,8 +150,10 @@ module E11y
138
150
  #
139
151
  # @example
140
152
  # adapter.event_count # Total events
141
- # adapter.event_count("order.paid") # Specific event count
142
- def event_count(event_name: nil)
153
+ # adapter.event_count("order.paid") # Specific event count (positional)
154
+ # adapter.event_count(event_name: "order.paid") # Specific event count (keyword)
155
+ def event_count(event_name = nil, **kwargs)
156
+ event_name ||= kwargs[:event_name]
143
157
  if event_name
144
158
  @events.count { |event| event[:event_name] == event_name }
145
159
  else
@@ -147,6 +161,16 @@ module E11y
147
161
  end
148
162
  end
149
163
 
164
+ # Get the most recently written event.
165
+ #
166
+ # @return [Hash, nil] The last event, or nil if none
167
+ #
168
+ # @example
169
+ # adapter.last_event # Most recently written event
170
+ def last_event
171
+ events.last
172
+ end
173
+
150
174
  # Get last N events
151
175
  #
152
176
  # @param count [Integer] Number of events to return
@@ -205,6 +229,28 @@ module E11y
205
229
 
206
230
  private
207
231
 
232
+ def event_pattern_for(pattern)
233
+ case pattern
234
+ when Class then pattern
235
+ when String, Regexp then pattern.is_a?(String) ? Regexp.new(Regexp.escape(pattern)) : pattern
236
+ else raise ArgumentError, "Pattern must be Class, String, or Regexp, got #{pattern.class}"
237
+ end
238
+ end
239
+
240
+ def event_matches?(event, pattern)
241
+ return event[:event_name].to_s.match?(pattern) if pattern.is_a?(Regexp)
242
+ return event_matches_class?(event, pattern) if pattern.is_a?(Class)
243
+
244
+ false
245
+ end
246
+
247
+ def event_matches_class?(event, klass)
248
+ event[:event_class] == klass ||
249
+ event[:event_class]&.name == klass.name ||
250
+ event[:event_name].to_s == (klass.respond_to?(:event_name) ? klass.event_name : klass.name) ||
251
+ event[:event_name].to_s.include?(klass.name)
252
+ end
253
+
208
254
  # Enforce max_events limit by dropping oldest events (FIFO)
209
255
  #
210
256
  # @return [void]
@@ -218,5 +264,6 @@ module E11y
218
264
  @dropped_count += excess
219
265
  end
220
266
  end
267
+ # rubocop:enable Metrics/ClassLength
221
268
  end
222
269
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "in_memory"
4
+
5
+ module E11y
6
+ module Adapters
7
+ # InMemoryTest Adapter — extends InMemory with test-specific helpers.
8
+ #
9
+ # Overrides `last_event` to skip Rails auto-instrumentation events
10
+ # (E11y::Events::Rails::*) that fire after each HTTP request and
11
+ # would otherwise obscure the event your test just tracked.
12
+ #
13
+ # Use this adapter in test suites; use `InMemory` in production configs.
14
+ #
15
+ # @example
16
+ # let(:adapter) { E11y::Adapters::InMemoryTest.new }
17
+ # before { E11y.register_adapter :memory, adapter }
18
+ class InMemoryTest < InMemory
19
+ # Return the last event that was NOT fired by Rails auto-instrumentation.
20
+ #
21
+ # @return [Hash, nil]
22
+ def last_event
23
+ events.reverse_each.find do |e|
24
+ !e[:event_name].to_s.start_with?("E11y::Events::Rails::")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -41,11 +41,8 @@ module E11y
41
41
  # batch_timeout: 5
42
42
  # )
43
43
  #
44
- # @example With Registry
45
- # E11y::Adapters::Registry.register(
46
- # :loki_logger,
47
- # E11y::Adapters::Loki.new(url: ENV["LOKI_URL"])
48
- # )
44
+ # @example Configuration
45
+ # config.adapters[:loki] = E11y::Adapters::Loki.new(url: ENV["LOKI_URL"])
49
46
  #
50
47
  # @example With Cardinality Protection (C04 Resolution - Enterprise)
51
48
  # # Enable for high-traffic environments to prevent label explosion
@@ -82,8 +79,9 @@ module E11y
82
79
  # @option config [Integer] :batch_timeout (5) Max seconds to wait before flushing batch
83
80
  # @option config [Boolean] :compress (true) Enable gzip compression
84
81
  # @option config [String] :tenant_id (nil) Loki tenant ID (X-Scope-OrgID header)
85
- # @option config [Boolean] :enable_cardinality_protection (false) Enable cardinality protection for labels (C04)
86
- # @option config [Integer] :max_label_cardinality (100) Max unique values per label when protection enabled
82
+ # @option config [Boolean] :enable_cardinality_protection (true) Enable cardinality protection for labels (C04)
83
+ # @option config [Integer] :max_label_cardinality (1000) Max unique values per label when protection enabled.
84
+ # Labels = event_name + severity only (payload stays in log line). 1000 covers ~1000 event types.
87
85
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
88
86
  # Adapter initialization requires many instance variable assignments
89
87
  def initialize(config = {})
@@ -91,20 +89,22 @@ module E11y
91
89
  @labels = config.fetch(:labels, {})
92
90
  @batch_size = config.fetch(:batch_size, DEFAULT_BATCH_SIZE)
93
91
  @batch_timeout = config.fetch(:batch_timeout, DEFAULT_BATCH_TIMEOUT)
92
+ @timeout = config.fetch(:timeout, 5)
93
+ @health_check_timeout = [@timeout, 2].min
94
94
  @compress = config.fetch(:compress, true)
95
95
  @tenant_id = config[:tenant_id]
96
- @enable_cardinality_protection = config.fetch(:enable_cardinality_protection, false)
97
- @max_label_cardinality = config.fetch(:max_label_cardinality, 100)
96
+ @enable_cardinality_protection = config.fetch(:enable_cardinality_protection, true)
97
+ @max_label_cardinality = config.fetch(:max_label_cardinality, 1000)
98
98
 
99
99
  @buffer = []
100
100
  @buffer_mutex = Mutex.new
101
101
  @connection = nil
102
102
  @last_flush = Time.now
103
103
 
104
- # C04: Optional cardinality protection (disabled by default for logs)
104
+ # C04: Cardinality protection for labels (enabled by default per ADR-009 §8)
105
105
  if @enable_cardinality_protection
106
106
  @cardinality_protection = E11y::Metrics::CardinalityProtection.new(
107
- max_unique_values: @max_label_cardinality
107
+ cardinality_limit: @max_label_cardinality
108
108
  )
109
109
  end
110
110
 
@@ -155,11 +155,22 @@ module E11y
155
155
  end
156
156
  end
157
157
 
158
- # Check if adapter is healthy
158
+ # Loki health check endpoint
159
+ READY_PATH = "/ready"
160
+
161
+ # Check if adapter is healthy (Loki server reachable)
162
+ #
163
+ # Performs actual HTTP GET to /ready. Returns false on connection failure,
164
+ # timeout, or non-2xx response.
159
165
  #
160
- # @return [Boolean] True if connection is established
166
+ # @return [Boolean] True if Loki responds with 2xx
161
167
  def healthy?
162
- @connection&.respond_to?(:get)
168
+ return false unless @connection
169
+
170
+ response = @connection.get(READY_PATH)
171
+ (200..299).cover?(response.status)
172
+ rescue Faraday::Error, Errno::ECONNREFUSED, Errno::ETIMEDOUT
173
+ false
163
174
  end
164
175
 
165
176
  # Adapter capabilities
@@ -194,7 +205,6 @@ module E11y
194
205
  #
195
206
  # @see ADR-004 Section 7.1 (Retry Policy via gem-level middleware)
196
207
  # @see ADR-004 Section 6.1 (Connection pooling via HTTP client)
197
- # rubocop:disable Metrics/MethodLength
198
208
  # HTTP client configuration requires detailed retry and connection settings
199
209
  def build_connection!
200
210
  @connection = Faraday.new(url: @url) do |f|
@@ -218,7 +228,6 @@ module E11y
218
228
  f.adapter Faraday.default_adapter
219
229
  end
220
230
  end
221
- # rubocop:enable Metrics/MethodLength
222
231
 
223
232
  # Check if buffer should be flushed
224
233
  def flush_if_needed!
@@ -280,22 +289,23 @@ module E11y
280
289
 
281
290
  # Extract labels from event
282
291
  #
292
+ # Uses normalized event_name (e.g., "Events::TestLoki" -> "test.loki") for consistent
293
+ # querying via LogQL. Matches Versioning middleware convention.
294
+ #
283
295
  # @param event_data [Hash] Event data
284
296
  # @return [Hash] Labels for Loki stream
285
297
  def extract_labels(event_data)
286
298
  event_labels = {
287
- event_name: event_data[:event_name].to_s,
299
+ event_name: normalize_event_name_for_labels(event_data[:event_name].to_s),
288
300
  severity: event_data[:severity].to_s
289
301
  }
290
302
 
291
303
  # Merge static and event labels
292
304
  all_labels = @labels.merge(event_labels)
293
305
 
294
- # C04: Apply cardinality protection if enabled (enterprise use case)
295
- # Disabled by default - Loki is a log system, labels are for stream filtering only
296
- if @enable_cardinality_protection && @cardinality_protection
297
- all_labels = @cardinality_protection.filter(all_labels, "loki.stream")
298
- end
306
+ # C04: Cardinality protection for labels only. Labels = event_name + severity (payload
307
+ # stays in log line). Filter by user_uuid via LogQL: | json | user_uuid="xxx"
308
+ all_labels = @cardinality_protection.filter(all_labels, "loki.stream") if @enable_cardinality_protection && @cardinality_protection
299
309
 
300
310
  all_labels.transform_keys(&:to_s)
301
311
  end
@@ -305,7 +315,17 @@ module E11y
305
315
  # @param event_data [Hash] Event data
306
316
  # @return [Array] [timestamp_ns, line]
307
317
  def format_loki_entry(event_data)
308
- timestamp_ns = (event_data[:timestamp] || Time.now).to_f * 1_000_000_000
318
+ # Parse timestamp - can be Time object, ISO8601 string, or nil
319
+ timestamp = event_data[:timestamp]
320
+ timestamp = if timestamp.is_a?(String)
321
+ Time.parse(timestamp)
322
+ elsif timestamp.nil?
323
+ Time.now
324
+ else
325
+ timestamp
326
+ end
327
+
328
+ timestamp_ns = timestamp.to_f * 1_000_000_000
309
329
  line = event_data.to_json
310
330
 
311
331
  [timestamp_ns.to_i.to_s, line]
@@ -323,6 +343,21 @@ module E11y
323
343
  io.string
324
344
  end
325
345
 
346
+ # Normalize event name for Loki labels (matches Versioning middleware convention)
347
+ #
348
+ # @param name [String] Event name (e.g., "Events::TestLoki")
349
+ # @return [String] Normalized name (e.g., "test.loki")
350
+ def normalize_event_name_for_labels(name)
351
+ return name if name.nil? || name.empty?
352
+
353
+ n = name.sub(/^Events::/, "").sub(/V\d+$/, "")
354
+ n.gsub("::", ".")
355
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
356
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
357
+ .downcase
358
+ .tr("_", ".")
359
+ end
360
+
326
361
  # Build HTTP headers
327
362
  #
328
363
  # @return [Hash] Headers for Loki request
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Adapters
5
+ # Null Adapter — silently discards all events.
6
+ #
7
+ # Designed for use in tests and development environments where you want
8
+ # to suppress all output while still being able to assert that events
9
+ # were tracked (via the `events` reader).
10
+ #
11
+ # @example In tests
12
+ # RSpec.configure do |config|
13
+ # config.before do
14
+ # E11y.configure do |c|
15
+ # c.adapters[:null] = E11y::Adapters::NullAdapter.new
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # @example Asserting events
21
+ # null_adapter = E11y::Adapters::NullAdapter.new
22
+ # E11y.configure { |c| c.adapters[:null] = null_adapter }
23
+ #
24
+ # Events::OrderPaid.track(order_id: "123", amount: 99.99)
25
+ #
26
+ # expect(null_adapter.events.size).to eq(1)
27
+ # expect(null_adapter.events.last[:event_name]).to eq("order.paid")
28
+ class Null < Base
29
+ attr_reader :events
30
+
31
+ # @param config [Hash] Options
32
+ # @option config [Boolean] :store_events (true) When false, truly discards (no retention).
33
+ # Use store_events: false for memory profiling to measure pipeline-only allocations.
34
+ def initialize(config = {})
35
+ super
36
+ @store_events = config.fetch(:store_events, true)
37
+ @events = []
38
+ @mutex = Mutex.new
39
+ end
40
+
41
+ # Accept event. When store_events: true, stores for inspection. When false, truly discards.
42
+ #
43
+ # @param event_data [Hash] Event payload
44
+ # @return [Boolean] always true
45
+ # rubocop:disable Naming/PredicateMethod -- implements Base adapter interface
46
+ def write(event_data)
47
+ @mutex.synchronize { @events << event_data.dup } if @store_events
48
+ true
49
+ end
50
+ # rubocop:enable Naming/PredicateMethod
51
+
52
+ # Accept batch. When store_events: true, stores for inspection. When false, truly discards.
53
+ #
54
+ # @param events [Array<Hash>] Event payloads
55
+ # @return [Boolean] always true
56
+ # rubocop:disable Naming/PredicateMethod -- implements Base adapter interface
57
+ def write_batch(events)
58
+ @mutex.synchronize { @events.concat(events.map(&:dup)) } if @store_events
59
+ true
60
+ end
61
+ # rubocop:enable Naming/PredicateMethod
62
+
63
+ # Clear all stored events (useful between test examples).
64
+ #
65
+ # @return [void]
66
+ def clear!
67
+ @mutex.synchronize { @events.clear }
68
+ end
69
+
70
+ def healthy?
71
+ true
72
+ end
73
+
74
+ def capabilities
75
+ { batching: true, compression: false, async: false, streaming: false, null: true }
76
+ end
77
+ end
78
+
79
+ # Convenience alias matching Quick Start documentation.
80
+ NullAdapter = Null
81
+ end
82
+ end