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
@@ -43,7 +43,7 @@ module E11y
43
43
  # # Events automatically update metrics via middleware
44
44
  #
45
45
  # @see ADR-002 Metrics & Yabeda Integration
46
- # @see UC-003 Pattern-Based Metrics
46
+ # @see UC-003 Event Metrics
47
47
  # rubocop:disable Metrics/ClassLength
48
48
  # Yabeda adapter contains metrics registration and update logic as cohesive unit
49
49
  class Yabeda < Base
@@ -67,6 +67,8 @@ module E11y
67
67
  return unless config.fetch(:auto_register, true)
68
68
 
69
69
  register_metrics_from_registry!
70
+ register_middleware_metrics!
71
+ register_self_monitoring_metrics!
70
72
 
71
73
  # Apply configuration in non-Rails environments (Rails does this automatically)
72
74
  # In tests, Yabeda.configure! should be called explicitly in before blocks
@@ -108,9 +110,10 @@ module E11y
108
110
 
109
111
  # Check if adapter is healthy
110
112
  #
111
- # @return [Boolean] true if Yabeda is available and configured
113
+ # @return [Boolean] true if Yabeda is available, configured, and e11y group exists
112
114
  def healthy?
113
115
  return false unless defined?(::Yabeda)
116
+ return false unless ::Yabeda.respond_to?(:e11y)
114
117
 
115
118
  ::Yabeda.configured?
116
119
  rescue StandardError
@@ -151,8 +154,11 @@ module E11y
151
154
  # Register metric if not exists
152
155
  register_metric_if_needed(name, :counter, safe_labels.keys)
153
156
 
154
- # Update Yabeda metric
155
- ::Yabeda.e11y.send(name).increment(safe_labels, by: value)
157
+ # Update Yabeda metric (guard against nil when metric wasn't registered, e.g. after configure!)
158
+ metric = ::Yabeda.e11y.send(name)
159
+ return unless metric
160
+
161
+ metric.increment(safe_labels, by: value)
156
162
  rescue StandardError => e
157
163
  E11y.logger.warn("Failed to increment Yabeda metric #{name}: #{e.message}")
158
164
  end
@@ -173,8 +179,11 @@ module E11y
173
179
  # Register metric if not exists
174
180
  register_metric_if_needed(name, :histogram, safe_labels.keys, buckets: buckets)
175
181
 
176
- # Update Yabeda metric
177
- ::Yabeda.e11y.send(name).measure(safe_labels, value)
182
+ # Update Yabeda metric (guard against nil when metric wasn't registered)
183
+ metric = ::Yabeda.e11y.send(name)
184
+ return unless metric
185
+
186
+ metric.measure(safe_labels, value)
178
187
  rescue StandardError => e
179
188
  E11y.logger.warn("Failed to observe Yabeda histogram #{name}: #{e.message}")
180
189
  end
@@ -194,8 +203,11 @@ module E11y
194
203
  # Register metric if not exists
195
204
  register_metric_if_needed(name, :gauge, safe_labels.keys)
196
205
 
197
- # Update Yabeda metric
198
- ::Yabeda.e11y.send(name).set(safe_labels, value)
206
+ # Update Yabeda metric (guard against nil when metric wasn't registered)
207
+ metric = ::Yabeda.e11y.send(name)
208
+ return unless metric
209
+
210
+ metric.set(safe_labels, value)
199
211
  rescue StandardError => e
200
212
  E11y.logger.warn("Failed to set Yabeda gauge #{name}: #{e.message}")
201
213
  end
@@ -208,9 +220,7 @@ module E11y
208
220
  super
209
221
 
210
222
  # Validate cardinality_limit
211
- if @config[:cardinality_limit] && !@config[:cardinality_limit].is_a?(Integer)
212
- raise ArgumentError, "cardinality_limit must be an Integer"
213
- end
223
+ raise ArgumentError, "cardinality_limit must be an Integer" if @config[:cardinality_limit] && !@config[:cardinality_limit].is_a?(Integer)
214
224
 
215
225
  # Validate forbidden_labels
216
226
  return unless @config[:forbidden_labels] && !@config[:forbidden_labels].is_a?(Array)
@@ -278,20 +288,112 @@ module E11y
278
288
  end
279
289
  end
280
290
 
291
+ # Pre-register middleware self-monitoring metrics.
292
+ #
293
+ # These metrics are used by TraceContext, Validation, and Routing middleware.
294
+ # Must be registered before Yabeda.configure! is called (e.g. in app initializers).
295
+ # Called during adapter initialization so they're available when events flow.
296
+ # Names use underscores (Prometheus requires /[a-zA-Z_:][a-zA-Z0-9_:]*/, no dots).
297
+ #
298
+ # @return [void]
299
+ def register_middleware_metrics!
300
+ return unless defined?(::Yabeda)
301
+
302
+ middleware_metrics = [
303
+ { name: :e11y_middleware_trace_context_processed, tags: [] },
304
+ { name: :e11y_middleware_validation_total, tags: [:result] },
305
+ { name: :e11y_middleware_routing_routed, tags: %i[adapters_count routing_type] }
306
+ ]
307
+
308
+ cardinality_metrics = [
309
+ { name: :e11y_cardinality_overflow_total, tags: %i[metric action strategy] },
310
+ { name: :e11y_cardinality_current, type: :gauge, tags: [:metric] }
311
+ ]
312
+
313
+ (middleware_metrics + cardinality_metrics).each do |m|
314
+ type = m[:type] || :counter
315
+ register_metric_if_needed(m[:name], type, m[:tags])
316
+ end
317
+ rescue StandardError => e
318
+ E11y.logger.debug("Could not register middleware metrics: #{e.message}")
319
+ end
320
+
321
+ # Pre-register self-monitoring metrics (request buffer, retry, circuit breaker, DLQ, etc.).
322
+ # Must be registered before Yabeda.configure! so they exist when reliability layer runs.
323
+ #
324
+ # @return [void] # -- metric list is inherently long
325
+ def register_self_monitoring_metrics!
326
+ return unless defined?(::Yabeda)
327
+
328
+ metrics = [
329
+ # Request buffer (consolidated)
330
+ { name: :e11y_ephemeral_buffer_total, tags: [:event] },
331
+ # Retry handler
332
+ { name: :e11y_retry_success, tags: %i[adapter attempts] },
333
+ { name: :e11y_retry_recovered, tags: %i[adapter attempts] },
334
+ { name: :e11y_retry_permanent_failure, tags: %i[adapter error attempt] },
335
+ { name: :e11y_retry_exhausted, tags: %i[adapter error attempts] },
336
+ { name: :e11y_retry_attempt, tags: %i[adapter error attempt] },
337
+ # Circuit breaker (consolidated: transitions counter + state gauge)
338
+ { name: :e11y_circuit_breaker_transitions_total, tags: %i[adapter event] },
339
+ { name: :e11y_circuit_breaker_state, type: :gauge, tags: [:adapter] },
340
+ # Adapter performance & reliability
341
+ { name: :e11y_adapter_send_duration_seconds, type: :histogram, tags: [:adapter], buckets: [0.001, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0] },
342
+ { name: :e11y_adapter_writes_total, tags: %i[adapter status error_class] },
343
+ # DLQ
344
+ { name: :e11y_dlq_size, type: :gauge, tags: [] },
345
+ { name: :e11y_dlq_filter_decisions_total, tags: %i[action reason] },
346
+ { name: :e11y_dlq_saved_total, tags: [:event_name] },
347
+ { name: :e11y_dlq_parse_error_total, tags: [:error] },
348
+ { name: :e11y_dlq_replayed_total, tags: [:event_name] },
349
+ { name: :e11y_dlq_replay_failed_total, tags: [:error] },
350
+ # Retry rate limiter (consolidated)
351
+ { name: :e11y_retry_rate_limiter_total, tags: %i[adapter event delay_sec] },
352
+ # Buffer (ring, adaptive) — consolidated
353
+ { name: :e11y_buffer_overflow_total, tags: [:event] },
354
+ # Rate limiting / sampling
355
+ { name: :e11y_events_dropped_total, tags: %i[reason event_type] },
356
+ # SLO tracking (Request middleware triggers on every HTTP request when enabled)
357
+ { name: :slo_http_requests_total, tags: %i[controller action status] },
358
+ { name: :slo_http_request_duration_seconds, type: :histogram, tags: %i[controller action],
359
+ buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] },
360
+ { name: :slo_background_jobs_total, tags: %i[job_class status queue] },
361
+ { name: :slo_background_job_duration_seconds, type: :histogram, tags: %i[job_class queue],
362
+ buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] },
363
+ # E11y self-monitoring (events tracked at pipeline end)
364
+ { name: :e11y_events_tracked_total, tags: %i[result event_name] },
365
+ # Track latency (TrackLatency middleware)
366
+ { name: :e11y_track_duration_seconds, type: :histogram, tags: %i[event_class severity result],
367
+ buckets: [0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1] }
368
+ ]
369
+
370
+ metrics.each do |m|
371
+ type = m[:type] || :counter
372
+ buckets = m[:buckets]
373
+ register_metric_if_needed(m[:name], type, m[:tags], buckets: buckets)
374
+ end
375
+ rescue StandardError => e
376
+ E11y.logger.debug("Could not register self-monitoring metrics: #{e.message}")
377
+ end
378
+
281
379
  # Register a single metric in Yabeda
282
380
  #
283
381
  # @param metric_config [Hash] Metric configuration from Registry
284
382
  # @return [void]
285
- # rubocop:disable Metrics/MethodLength
286
383
  # Metric registration requires case/when for different metric types
287
384
  def register_yabeda_metric(metric_config)
288
385
  metric_name = metric_config[:name]
289
386
  metric_type = metric_config[:type]
290
387
  tags = metric_config[:tags] || []
291
388
 
389
+ # Skip if metric already exists (prevents re-registration errors)
390
+ return if ::Yabeda.metrics.key?("e11y_#{metric_name}")
391
+
292
392
  # Define metric in Yabeda group
293
- ::Yabeda.configure do
294
- group :e11y do
393
+ ::Yabeda.configure do |config = nil|
394
+ next unless config.respond_to?(:group)
395
+
396
+ config.group :e11y do
295
397
  case metric_type
296
398
  when :counter
297
399
  counter metric_name, tags: tags, comment: "E11y metric: #{metric_name}"
@@ -309,7 +411,6 @@ module E11y
309
411
  # Metric might already be registered - that's OK
310
412
  warn "E11y Yabeda: Could not register metric #{metric_name}: #{e.message}"
311
413
  end
312
- # rubocop:enable Metrics/MethodLength
313
414
 
314
415
  # Register a metric if it doesn't exist yet (for direct metric calls).
315
416
  #
@@ -319,14 +420,15 @@ module E11y
319
420
  # @param buckets [Array<Numeric>, nil] Optional histogram buckets
320
421
  # @return [void]
321
422
  # @api private
322
- # rubocop:disable Metrics/MethodLength
323
423
  # Metric registration requires case/when for different metric types
324
424
  def register_metric_if_needed(name, type, tags, buckets: nil)
325
- # Check if metric already exists
326
- return if ::Yabeda.metrics.key?(:"e11y_#{name}")
425
+ # Check if metric already exists (Yabeda stores metric keys as strings)
426
+ return if ::Yabeda.metrics.key?("e11y_#{name}")
327
427
 
328
- ::Yabeda.configure do
329
- group :e11y do
428
+ ::Yabeda.configure do |config = nil|
429
+ next unless config.respond_to?(:group)
430
+
431
+ config.group :e11y do
330
432
  case type
331
433
  when :counter
332
434
  counter name, tags: tags, comment: "E11y self-monitoring: #{name}"
@@ -347,38 +449,60 @@ module E11y
347
449
  # Metric might already be registered - that's OK
348
450
  E11y.logger.warn("Could not register Yabeda metric #{name}: #{e.message}")
349
451
  end
350
- # rubocop:enable Metrics/MethodLength
351
452
 
352
453
  # Update a single metric based on event data
353
454
  #
354
455
  # @param metric_config [Hash] Metric configuration
355
456
  # @param event_data [Hash] Event data
356
457
  # @return [void]
357
- # rubocop:disable Metrics/AbcSize
358
458
  # Metric update requires multiple steps for label extraction and value handling
359
459
  def update_metric(metric_config, event_data)
360
460
  metric_name = metric_config[:name]
361
461
  labels = extract_labels(metric_config, event_data)
362
462
 
363
- # Apply cardinality protection
364
- safe_labels = @cardinality_protection.filter(labels, metric_name)
463
+ # Apply cardinality protection (normalize metric_name to string for consistent tracking)
464
+ safe_labels = @cardinality_protection.filter(labels, metric_name.to_s)
365
465
 
366
466
  # Extract value for histogram/gauge
367
467
  value = extract_value(metric_config, event_data) if %i[histogram gauge].include?(metric_config[:type])
368
468
 
369
- # Update Yabeda metric
469
+ # Get original tags from metric config - these are the tags the metric was registered with
470
+ original_tags = metric_config.fetch(:tags, [])
471
+
472
+ # Lazy registration: register metric if it doesn't exist in Yabeda yet
473
+ # CRITICAL: Use ORIGINAL tags from metric config, not filtered safe_labels.keys
474
+ # Prometheus requires all tags declared at registration time
475
+ register_metric_if_needed(
476
+ metric_name,
477
+ metric_config[:type],
478
+ original_tags,
479
+ buckets: metric_config[:buckets]
480
+ )
481
+
482
+ # Ensure all required tags are present in safe_labels
483
+ # If cardinality protection dropped a tag, add placeholder value
484
+ # Prometheus requires all tags declared at registration to be present in every update
485
+ final_labels = original_tags.to_h do |tag|
486
+ [tag, safe_labels.key?(tag) ? safe_labels[tag] : "[DROPPED]"]
487
+ end
488
+
489
+ # Update Yabeda metric (skip if e11y group not registered, e.g. Yabeda not configured)
490
+ return unless ::Yabeda.respond_to?(:e11y)
491
+
492
+ metric = ::Yabeda.e11y.send(metric_name)
493
+ return unless metric
494
+
370
495
  case metric_config[:type]
371
496
  when :counter
372
- ::Yabeda.e11y.send(metric_name).increment(safe_labels)
497
+ metric.increment(final_labels)
373
498
  when :histogram
374
- ::Yabeda.e11y.send(metric_name).measure(safe_labels, value)
499
+ metric.measure(final_labels, value)
375
500
  when :gauge
376
- ::Yabeda.e11y.send(metric_name).set(safe_labels, value)
501
+ metric.set(final_labels, value)
377
502
  end
378
503
  rescue StandardError => e
379
504
  warn "E11y Yabeda: Error updating metric #{metric_name}: #{e.message}"
380
505
  end
381
- # rubocop:enable Metrics/AbcSize
382
506
 
383
507
  # Extract labels from event data
384
508
  #
@@ -247,7 +247,6 @@ module E11y
247
247
  #
248
248
  # @param obj [Object] Payload object
249
249
  # @return [Integer] Size in bytes
250
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
251
250
  def calculate_payload_size(obj)
252
251
  case obj
253
252
  when String
@@ -266,14 +265,12 @@ module E11y
266
265
  rescue StandardError
267
266
  500 # Fallback for errors
268
267
  end
269
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
270
268
 
271
269
  # Handle memory exhaustion according to strategy
272
270
  #
273
271
  # @param event_data [Hash] Event that caused exhaustion
274
272
  # @param event_size [Integer] Size of event
275
273
  # @return [Boolean] true if event was eventually added, false if dropped
276
- # rubocop:disable Metrics/MethodLength
277
274
  def handle_memory_exhaustion(event_data, event_size)
278
275
  case @backpressure_strategy
279
276
  when :block
@@ -288,7 +285,7 @@ module E11y
288
285
  # Check timeout
289
286
  if Time.now - wait_start > @max_block_time
290
287
  # Timeout exceeded - drop event
291
- increment_metric("e11y.buffer.memory_exhaustion.dropped")
288
+ E11y::Metrics.increment(:e11y_buffer_overflow_total, event: "memory_exhaustion_dropped")
292
289
  return false
293
290
  end
294
291
 
@@ -297,16 +294,15 @@ module E11y
297
294
  end
298
295
 
299
296
  # Space available - retry add
300
- increment_metric("e11y.buffer.memory_exhaustion.blocked")
297
+ E11y::Metrics.increment(:e11y_buffer_overflow_total, event: "memory_exhaustion_blocked")
301
298
  add_event(event_data)
302
299
 
303
300
  when :drop
304
301
  # Drop new event
305
- increment_metric("e11y.buffer.memory_exhaustion.dropped")
302
+ E11y::Metrics.increment(:e11y_buffer_overflow_total, event: "memory_exhaustion_dropped")
306
303
  false
307
304
  end
308
305
  end
309
- # rubocop:enable Metrics/MethodLength
310
306
 
311
307
  # Trigger early flush (80% threshold reached)
312
308
  #
@@ -323,16 +319,6 @@ module E11y
323
319
  warn "E11y: Early flush callback failed: #{e.message}"
324
320
  end
325
321
 
326
- # Increment metric (placeholder for Phase 3: Metrics)
327
- #
328
- # TODO Phase 3: Replace with actual Yabeda metrics
329
- #
330
- # @param metric_name [String] Metric to increment
331
- # @return [void]
332
- def increment_metric(metric_name)
333
- # Placeholder - will be implemented in Phase 3
334
- # Yabeda.e11y.buffer_memory_exhaustion.increment(strategy: @backpressure_strategy)
335
- end
336
322
  # rubocop:enable Metrics/ClassLength
337
323
  end
338
324
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module E11y
4
4
  module Buffers
5
- # Request-scoped buffer using thread-local storage for debug event buffering.
5
+ # Ephemeral buffer (request/job-scoped) using thread-local storage for debug event buffering.
6
6
  #
7
7
  # This buffer stores debug events in thread-local storage during request processing.
8
8
  # Events are flushed only when an error occurs, keeping logs clean during successful requests.
@@ -12,21 +12,21 @@ module E11y
12
12
  #
13
13
  # @example Basic usage
14
14
  # # In Rails middleware
15
- # RequestScopedBuffer.initialize!
15
+ # EphemeralBuffer.initialize!
16
16
  #
17
17
  # # Track debug events (buffered)
18
- # RequestScopedBuffer.add_event({ event_name: "debug", severity: :debug })
18
+ # EphemeralBuffer.add_event({ event_name: "debug", severity: :debug })
19
19
  #
20
20
  # # On error - flush all buffered events
21
- # RequestScopedBuffer.flush_on_error
21
+ # EphemeralBuffer.flush_on_error
22
22
  #
23
23
  # # On success - discard buffered events
24
- # RequestScopedBuffer.discard
24
+ # EphemeralBuffer.discard
25
25
  #
26
26
  # @see UC-001 Request-Scoped Debug Buffering
27
- class RequestScopedBuffer
27
+ class EphemeralBuffer
28
28
  # Thread-local storage keys
29
- THREAD_KEY_BUFFER = :e11y_request_buffer
29
+ THREAD_KEY_BUFFER = :e11y_ephemeral_buffer
30
30
  THREAD_KEY_REQUEST_ID = :e11y_request_id
31
31
  THREAD_KEY_ERROR_OCCURRED = :e11y_error_occurred
32
32
  THREAD_KEY_BUFFER_LIMIT = :e11y_buffer_limit
@@ -42,7 +42,7 @@ module E11y
42
42
  # @return [void]
43
43
  #
44
44
  # @example
45
- # RequestScopedBuffer.initialize!(request_id: "req-123", buffer_limit: 200)
45
+ # EphemeralBuffer.initialize!(request_id: "req-123", buffer_limit: 200)
46
46
  def initialize!(request_id: nil, buffer_limit: DEFAULT_BUFFER_LIMIT)
47
47
  Thread.current[THREAD_KEY_BUFFER] = []
48
48
  Thread.current[THREAD_KEY_REQUEST_ID] = request_id || generate_request_id
@@ -60,42 +60,19 @@ module E11y
60
60
  #
61
61
  # @example
62
62
  # # Debug event - buffered
63
- # RequestScopedBuffer.add_event({ event_name: "test", severity: :debug })
63
+ # EphemeralBuffer.add_event({ event_name: "test", severity: :debug })
64
64
  # # => true
65
65
  #
66
66
  # # Error event - not buffered, triggers flush
67
- # RequestScopedBuffer.add_event({ event_name: "error", severity: :error })
67
+ # EphemeralBuffer.add_event({ event_name: "error", severity: :error })
68
68
  # # => false (and flushes buffer)
69
- # rubocop:disable Metrics/MethodLength, Naming/PredicateMethod
70
69
  def add_event(event_data)
71
- return false unless active? # Not in request scope
70
+ return false unless active?
71
+ return handle_error_event(event_data) if error_severity?(event_data[:severity])
72
+ return false unless event_data[:severity] == :debug
72
73
 
73
- severity = event_data[:severity]
74
-
75
- # Trigger flush on error severity
76
- if error_severity?(severity)
77
- Thread.current[THREAD_KEY_ERROR_OCCURRED] = true
78
- flush_on_error
79
- return false # Error events not buffered
80
- end
81
-
82
- # Only buffer debug events
83
- return false unless severity == :debug
84
-
85
- current_buffer = buffer
86
- return false if current_buffer.nil?
87
-
88
- # Check buffer limit
89
- if current_buffer.size >= buffer_limit
90
- increment_metric("e11y.request_buffer.overflow")
91
- return false # Buffer full, drop event
92
- end
93
-
94
- current_buffer << event_data
95
- increment_metric("e11y.request_buffer.events_buffered")
96
- true
74
+ append_to_buffer(event_data)
97
75
  end
98
- # rubocop:enable Metrics/MethodLength, Naming/PredicateMethod
99
76
 
100
77
  # Flush buffered events on error
101
78
  #
@@ -108,7 +85,7 @@ module E11y
108
85
  # @example
109
86
  # # In rescue block
110
87
  # rescue StandardError => e
111
- # RequestScopedBuffer.flush_on_error
88
+ # EphemeralBuffer.flush_on_error
112
89
  # raise
113
90
  # end
114
91
  def flush_on_error(target: nil)
@@ -117,15 +94,18 @@ module E11y
117
94
 
118
95
  flushed_count = current_buffer.size
119
96
 
97
+ # Resolve flush targets once per flush (avoids N config lookups when flushing N events)
98
+ flush_targets = resolve_flush_targets
99
+
120
100
  # Flush events to main buffer/adapters
121
101
  current_buffer.each do |event_data|
122
102
  # TODO: Send to E11y::Collector.collect(event_data) when available
123
103
  # For now, placeholder
124
- flush_event(event_data, target: target)
104
+ flush_event(event_data, target: target, flush_targets: flush_targets)
125
105
  end
126
106
 
127
107
  current_buffer.clear
128
- increment_metric("e11y.request_buffer.flushed_on_error", tags: { events: flushed_count })
108
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "flushed_on_error", value: flushed_count)
129
109
  flushed_count
130
110
  end
131
111
 
@@ -135,8 +115,8 @@ module E11y
135
115
  #
136
116
  # @example
137
117
  # # In middleware ensure block (success path)
138
- # unless RequestScopedBuffer.error_occurred?
139
- # RequestScopedBuffer.discard
118
+ # unless EphemeralBuffer.error_occurred?
119
+ # EphemeralBuffer.discard
140
120
  # end
141
121
  def discard
142
122
  current_buffer = buffer
@@ -144,7 +124,7 @@ module E11y
144
124
 
145
125
  discarded_count = current_buffer.size
146
126
  current_buffer.clear
147
- increment_metric("e11y.request_buffer.discarded", tags: { events: discarded_count })
127
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "discarded", value: discarded_count)
148
128
  discarded_count
149
129
  end
150
130
 
@@ -195,6 +175,28 @@ module E11y
195
175
 
196
176
  private
197
177
 
178
+ def handle_error_event(_event_data) # rubocop:disable Naming/PredicateMethod
179
+ Thread.current[THREAD_KEY_ERROR_OCCURRED] = true
180
+ flush_on_error
181
+ false
182
+ end
183
+
184
+ def append_to_buffer(event_data)
185
+ current_buffer = buffer
186
+ return false if current_buffer.nil?
187
+ return record_buffer_overflow if current_buffer.size >= buffer_limit
188
+
189
+ event_to_store = event_data.merge(request_id: request_id)
190
+ current_buffer << event_to_store
191
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "events_buffered")
192
+ true
193
+ end
194
+
195
+ def record_buffer_overflow # rubocop:disable Naming/PredicateMethod
196
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "overflow")
197
+ false
198
+ end
199
+
198
200
  # Get buffer limit (with fallback)
199
201
  #
200
202
  # @return [Integer] Buffer limit
@@ -218,27 +220,39 @@ module E11y
218
220
  SecureRandom.uuid
219
221
  end
220
222
 
221
- # Flush single event to adapters
223
+ # Resolve flush targets: adapter instances (when debug_adapters set) or nil (use pipeline).
224
+ # Cached per flush to avoid repeated config lookups when flushing N events.
222
225
  #
223
- # @param event_data [Hash] Event to flush
224
- # @param target [Symbol, nil] Optional target adapter (not yet implemented)
225
- # @return [void]
226
- def flush_event(_event_data, target: nil) # rubocop:disable Lint/UnusedMethodArgument
227
- # Placeholder for E11y::Collector integration
228
- # Will be implemented when Collector/Adapter classes are available
226
+ # @return [Array<Object>, nil] Adapter instances to write to, or nil to use pipeline
227
+ def resolve_flush_targets
228
+ da = E11y.config.ephemeral_buffer_debug_adapters
229
+ return nil unless da&.any?
229
230
 
230
- # For now, just increment metric
231
- increment_metric("e11y.request_buffer.event_flushed")
231
+ da.filter_map { |name| E11y.configuration.adapters[name] }
232
232
  end
233
233
 
234
- # Increment metric (placeholder)
234
+ # Flush single event to adapters via pipeline or debug_adapters
235
235
  #
236
- # @param metric_name [String] Metric name
237
- # @param tags [Hash] Optional tags
236
+ # When config.ephemeral_buffer_debug_adapters is set, sends directly to those
237
+ # adapters. Otherwise uses the full pipeline (fallback_adapters).
238
+ #
239
+ # @param event_data [Hash] Event to flush
240
+ # @param target [Symbol, nil] Optional target adapter (not yet implemented)
241
+ # @param flush_targets [Array<Object>, nil] Pre-resolved adapter instances (from flush_on_error)
238
242
  # @return [void]
239
- def increment_metric(metric_name, tags: {})
240
- # Placeholder for Yabeda integration
241
- # Will be implemented in Phase 1 L2.4 (Metrics)
243
+ def flush_event(event_data, target: nil, flush_targets: nil) # rubocop:disable Lint/UnusedMethodArgument
244
+ return unless event_data
245
+
246
+ event_to_send = event_data.merge(from_ephemeral_buffer_flush: true)
247
+ targets = flush_targets || resolve_flush_targets
248
+
249
+ if targets
250
+ targets.each { |adapter| adapter&.write(event_to_send) }
251
+ else
252
+ E11y.config.built_pipeline.call(event_to_send)
253
+ end
254
+
255
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "event_flushed")
242
256
  end
243
257
  end
244
258
  end
@@ -165,7 +165,7 @@ module E11y
165
165
  # @return [Array<Hash>] All buffered events
166
166
  #
167
167
  # @example
168
- # all_events = buffer.flush_all
168
+ # events = buffer.flush_all
169
169
  # # => [event1, event2, ...]
170
170
  def flush_all
171
171
  pop(@size.value)
@@ -208,7 +208,6 @@ module E11y
208
208
  #
209
209
  # @param event [Hash] Event that caused overflow
210
210
  # @return [Boolean] true if event was eventually added, false if dropped
211
- # rubocop:disable Metrics/MethodLength
212
211
  def handle_overflow(event)
213
212
  case @overflow_strategy
214
213
  when :drop_oldest
@@ -217,21 +216,20 @@ module E11y
217
216
  push(event) # Retry push (recursive, but will succeed)
218
217
  when :drop_newest
219
218
  # Drop new event, keep buffer unchanged
220
- increment_metric("e11y.buffer.overflow.drop_newest")
219
+ E11y::Metrics.increment(:e11y_buffer_overflow_total, event: "drop_newest")
221
220
  false
222
221
  when :block
223
222
  # Wait for space, with timeout
224
223
  wait_for_space
225
224
  if full?
226
225
  # Timeout reached, drop event
227
- increment_metric("e11y.buffer.overflow.block_timeout")
226
+ E11y::Metrics.increment(:e11y_buffer_overflow_total, event: "block_timeout")
228
227
  false
229
228
  else
230
229
  push(event) # Retry after space freed
231
230
  end
232
231
  end
233
232
  end
234
- # rubocop:enable Metrics/MethodLength
235
233
 
236
234
  # Wait for buffer space (with timeout)
237
235
  #
@@ -251,17 +249,6 @@ module E11y
251
249
  sleep 0.001 # 1ms
252
250
  end
253
251
  end
254
-
255
- # Increment metric (placeholder for Phase 3: Metrics)
256
- #
257
- # TODO Phase 3: Replace with actual Yabeda metrics
258
- #
259
- # @param metric_name [String] Metric to increment
260
- # @return [void]
261
- def increment_metric(metric_name)
262
- # Placeholder - will be implemented in Phase 3
263
- # Yabeda.e11y.buffer_overflow.increment(strategy: @overflow_strategy)
264
- end
265
252
  end
266
253
  end
267
254
  end