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,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Middleware
5
+ # Measures Event.track() latency from pipeline entry to exit.
6
+ #
7
+ # Must be the FIRST middleware so it wraps the entire pipeline.
8
+ # Records duration for both success and dropped events.
9
+ #
10
+ # @see ADR-016 §3.1 (Performance Metrics)
11
+ # @example Add first in pipeline
12
+ # config.pipeline.use E11y::Middleware::TrackLatency
13
+ # config.pipeline.use E11y::Middleware::TraceContext
14
+ # # ...
15
+ class TrackLatency < Base
16
+ middleware_zone :pre_processing
17
+
18
+ def call(event_data)
19
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ result = @app.call(event_data)
21
+ duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
22
+
23
+ E11y::SelfMonitoring::PerformanceMonitor.track_latency(
24
+ duration_ms,
25
+ event_class: event_data[:event_name].to_s,
26
+ severity: event_data[:severity].to_s,
27
+ result: result.nil? ? :dropped : :success
28
+ )
29
+
30
+ result
31
+ end
32
+ end
33
+ end
34
+ end
@@ -56,7 +56,7 @@ module E11y
56
56
  # @option event_data [Hash] :payload The event payload (required)
57
57
  # @return [Hash, nil] Validated event data, or nil if dropped
58
58
  # @raise [E11y::ValidationError] if validation fails
59
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
59
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
60
60
  def call(event_data)
61
61
  # Skip validation if no event_class or payload
62
62
  return @app.call(event_data) unless event_data[:event_class] && event_data[:payload]
@@ -69,7 +69,7 @@ module E11y
69
69
 
70
70
  # Skip validation if mode is :never
71
71
  if validation_mode == :never
72
- increment_metric("e11y.middleware.validation.skipped")
72
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
73
73
  return @app.call(event_data)
74
74
  end
75
75
 
@@ -77,7 +77,7 @@ module E11y
77
77
  if validation_mode == :sampled
78
78
  sample_rate = event_class.respond_to?(:validation_sample_rate) ? event_class.validation_sample_rate : 0.01
79
79
  if rand >= sample_rate
80
- increment_metric("e11y.middleware.validation.skipped")
80
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
81
81
  return @app.call(event_data)
82
82
  end
83
83
  end
@@ -87,7 +87,7 @@ module E11y
87
87
 
88
88
  # Skip validation if no schema defined (schema-less events)
89
89
  unless schema
90
- increment_metric("e11y.middleware.validation.skipped")
90
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
91
91
  return @app.call(event_data)
92
92
  end
93
93
 
@@ -96,17 +96,17 @@ module E11y
96
96
 
97
97
  if result.success?
98
98
  # Validation passed
99
- increment_metric("e11y.middleware.validation.passed")
99
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "passed")
100
100
  @app.call(event_data)
101
101
  else
102
102
  # Validation failed - raise error with details
103
- increment_metric("e11y.middleware.validation.failed")
103
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "failed")
104
104
 
105
105
  error_message = format_validation_errors(event_class, result.errors)
106
106
  raise E11y::ValidationError, error_message
107
107
  end
108
108
  end
109
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
109
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
110
110
 
111
111
  private
112
112
 
@@ -122,15 +122,6 @@ module E11y
122
122
 
123
123
  "Validation failed for #{event_class.name}: #{error_details}"
124
124
  end
125
-
126
- # Placeholder for metrics instrumentation.
127
- #
128
- # @param metric_name [String] Metric name
129
- # @return [void]
130
- def increment_metric(_metric_name)
131
- # TODO: Integrate with Yabeda/Prometheus in Phase 2
132
- # Yabeda.e11y.middleware_validation_passed.increment
133
- end
134
125
  end
135
126
  end
136
127
  end
@@ -54,43 +54,47 @@ module E11y
54
54
  # @see ADR-012 for versioning architecture
55
55
  # @see UC-020 for use cases
56
56
  class Versioning < Base
57
- # Version extraction regex (matches V2, V3, etc. at end of class name)
58
- VERSION_REGEX = /V(\d+)$/
57
+ middleware_zone :pre_processing
58
+ VERSION_REGEX = E11y::Versioning::VersionExtractor::VERSION_REGEX
59
+
60
+ # Lazy cache: class name -> normalized event_name (per class, immutable)
61
+ NORMALIZED_CACHE = Concurrent::Map.new
59
62
 
60
63
  # Process event and add version field if needed
61
64
  #
62
65
  # @param event_data [Hash] Event payload
63
66
  # @return [Hash] Event data with version field (if > 1)
64
67
  def call(event_data)
65
- # Extract version from event_name (class name)
66
- version = extract_version(event_data[:event_name])
68
+ klass = event_data[:event_class]
69
+ class_name = klass&.name
67
70
 
68
- # Add version field only if > 1 (ADR-012 §4.2)
71
+ version = event_data[:version].to_i
72
+ version = extract_version(class_name) if version <= 1
69
73
  event_data[:v] = version if version > 1
70
74
 
71
- # Normalize event_name (remove version suffix for consistent queries)
72
- event_data[:event_name] = normalize_event_name(event_data[:event_name])
75
+ # event_data[:event_name] set by Base; fallback to klass.event_name for minimal event_data (tests)
76
+ incoming = event_data[:event_name]
77
+ incoming = klass.event_name if incoming.nil? && klass.respond_to?(:event_name)
78
+ incoming = incoming.to_s
79
+ # Custom uses dot notation ("order.paid"); default from Base uses "::"
80
+ event_data[:event_name] = incoming != "" && !incoming.include?("::") ? incoming : normalized_for(klass)
73
81
 
74
- event_data
82
+ @app&.call(event_data) || event_data
75
83
  end
76
84
 
77
85
  private
78
86
 
79
- # Extract version from event class name
80
- #
81
- # @param class_name [String] Event class name (e.g., "Events::OrderPaidV2")
82
- # @return [Integer] Version number (default: 1)
83
- #
84
- # @example
85
- # extract_version("Events::OrderPaid") => 1
86
- # extract_version("Events::OrderPaidV2") => 2
87
- # extract_version("Events::OrderPaidV3") => 3
88
- def extract_version(class_name)
89
- return 1 unless class_name
87
+ def normalized_for(klass)
88
+ return unless klass
90
89
 
91
- # Extract version from class name (e.g., "Events::OrderPaidV2" → 2)
92
- match = class_name.match(VERSION_REGEX)
93
- match ? match[1].to_i : 1
90
+ name = klass.name
91
+ return unless name
92
+
93
+ NORMALIZED_CACHE.fetch(name) { NORMALIZED_CACHE[name] = normalize_event_name(name) }
94
+ end
95
+
96
+ def extract_version(class_name)
97
+ E11y::Versioning::VersionExtractor.extract_version(class_name)
94
98
  end
95
99
 
96
100
  # Normalize event_name by removing version suffix
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module OpenTelemetry
5
+ # Semantic conventions mapper for OTel attributes (ADR-007 §4, F4).
6
+ #
7
+ # Maps E11y payload keys to OpenTelemetry semantic convention attribute names.
8
+ # When event_name matches a convention type (http, database, etc.), known keys
9
+ # are mapped to semantic names (e.g. method → http.method).
10
+ #
11
+ # @see https://opentelemetry.io/docs/specs/semconv/
12
+ class SemanticConventions
13
+ # Key mappings by convention type
14
+ # https://opentelemetry.io/docs/specs/semconv/http/
15
+ # https://opentelemetry.io/docs/specs/semconv/database/
16
+ # https://opentelemetry.io/docs/specs/semconv/exceptions/
17
+ CONVENTIONS = {
18
+ http: {
19
+ "method" => "http.method",
20
+ "route" => "http.route",
21
+ "path" => "http.target",
22
+ "status_code" => "http.status_code",
23
+ "status" => "http.status_code",
24
+ "duration_ms" => "http.server.duration",
25
+ "request_size" => "http.request.body.size",
26
+ "response_size" => "http.response.body.size",
27
+ "user_agent" => "http.user_agent",
28
+ "client_ip" => "http.client_ip",
29
+ "scheme" => "http.scheme",
30
+ "host" => "http.host",
31
+ "server_name" => "http.server_name"
32
+ },
33
+ database: {
34
+ "query" => "db.statement",
35
+ "statement" => "db.statement",
36
+ "duration_ms" => "db.operation.duration",
37
+ "rows_affected" => "db.operation.rows_affected",
38
+ "connection_id" => "db.connection.id",
39
+ "database_name" => "db.name",
40
+ "table_name" => "db.sql.table",
41
+ "operation" => "db.operation"
42
+ },
43
+ rpc: {
44
+ "service" => "rpc.service",
45
+ "method" => "rpc.method",
46
+ "system" => "rpc.system",
47
+ "status_code" => "rpc.grpc.status_code"
48
+ },
49
+ messaging: {
50
+ "queue_name" => "messaging.destination.name",
51
+ "message_id" => "messaging.message.id",
52
+ "conversation_id" => "messaging.message.conversation_id",
53
+ "payload_size" => "messaging.message.payload_size_bytes",
54
+ "operation" => "messaging.operation"
55
+ },
56
+ exception: {
57
+ "error_type" => "exception.type",
58
+ "error_message" => "exception.message",
59
+ "error_class" => "exception.type",
60
+ "stacktrace" => "exception.stacktrace"
61
+ }
62
+ }.freeze
63
+
64
+ # Map payload keys to OTel semantic attribute names.
65
+ #
66
+ # @param event_name [String] Event name (used to detect convention type)
67
+ # @param payload [Hash] Event payload
68
+ # @return [Hash] Mapped payload with semantic keys where applicable
69
+ def self.map(event_name, payload)
70
+ convention_type = detect_convention_type(event_name)
71
+ return payload.transform_keys { |k| "event.#{k}" } unless convention_type
72
+
73
+ conventions = CONVENTIONS[convention_type]
74
+ payload.each_with_object({}) do |(key, value), mapped|
75
+ otel_key = conventions[key.to_s] || "event.#{key}"
76
+ mapped[otel_key] = value
77
+ end
78
+ end
79
+
80
+ # Map a single key to OTel semantic attribute name.
81
+ #
82
+ # @param event_name [String] Event name (used to detect convention type)
83
+ # @param key [String, Symbol] Payload key
84
+ # @return [String] OTel attribute key
85
+ def self.map_key(event_name, key)
86
+ convention_type = detect_convention_type(event_name)
87
+ return "event.#{key}" unless convention_type
88
+
89
+ conventions = CONVENTIONS[convention_type]
90
+ conventions[key.to_s] || "event.#{key}"
91
+ end
92
+
93
+ # Detect convention type from event name
94
+ #
95
+ # @param event_name [String]
96
+ # @return [Symbol, nil]
97
+ def self.detect_convention_type(event_name)
98
+ name = event_name.to_s
99
+ return :http if name.match?(/http|request|response/i)
100
+ return :database if name.match?(/database|query|sql|postgres|mysql/i)
101
+ return :rpc if name.match?(/rpc|grpc/i)
102
+ return :messaging if name.match?(/message|queue|kafka|rabbitmq|sidekiq|job/i)
103
+ return :exception if name.match?(/error|exception|failure/i)
104
+
105
+ nil
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/opentelemetry/semantic_conventions"
4
+
5
+ module E11y
6
+ module OpenTelemetry
7
+ # Creates OpenTelemetry spans from E11y events (ADR-007 §6, F2).
8
+ #
9
+ # When enabled via config.opentelemetry_span_creation_patterns, creates
10
+ # OTel spans for matching events. Errors/fatal always create spans.
11
+ # Uses SemanticConventions for attribute mapping when applicable.
12
+ #
13
+ # @example Configuration
14
+ # E11y.configure do |config|
15
+ # config.opentelemetry_span_creation_patterns = ["order.*", "payment.*"]
16
+ # end
17
+ #
18
+ # @see ADR-007 §6 Traces Signal Export
19
+ # @see E11y::OpenTelemetry::SemanticConventions
20
+ class SpanCreator
21
+ ATTR_EVENT_NAME = "event.name"
22
+ ATTR_SEVERITY = "event.severity"
23
+ ATTR_E11Y_TRACE_ID = "e11y.trace_id"
24
+ ATTR_E11Y_SPAN_ID = "e11y.span_id"
25
+
26
+ class << self
27
+ def create_span_from_event(event_data)
28
+ return unless defined?(::OpenTelemetry::Trace)
29
+ return unless should_create_span?(event_data)
30
+
31
+ tracer = ::OpenTelemetry.tracer_provider.tracer("e11y", E11y::VERSION)
32
+ parent_ctx = ::OpenTelemetry::Context.current
33
+ start_ts = time_to_nano(event_data[:timestamp] || Time.now)
34
+
35
+ span = tracer.start_span(
36
+ span_name(event_data),
37
+ with_parent: parent_ctx,
38
+ kind: span_kind(event_data),
39
+ start_timestamp: start_ts
40
+ )
41
+
42
+ set_attributes(span, event_data)
43
+ set_status(span, event_data)
44
+ record_exception(span, event_data) if event_data[:severity].in?(%i[error fatal])
45
+
46
+ end_ts = compute_end_timestamp(event_data)
47
+ span.finish(end_timestamp: end_ts)
48
+
49
+ span
50
+ end
51
+
52
+ private
53
+
54
+ def span_name(event_data)
55
+ event_data[:event_name].to_s.presence || "e11y.event"
56
+ end
57
+
58
+ def set_attributes(span, event_data)
59
+ span.set_attribute(ATTR_EVENT_NAME, event_data[:event_name].to_s)
60
+ span.set_attribute(ATTR_SEVERITY, event_data[:severity].to_s)
61
+ span.set_attribute(ATTR_E11Y_TRACE_ID, event_data[:trace_id].to_s) if event_data[:trace_id]
62
+ span.set_attribute(ATTR_E11Y_SPAN_ID, event_data[:span_id].to_s) if event_data[:span_id]
63
+
64
+ payload = event_data[:payload] || {}
65
+ return if payload.empty?
66
+
67
+ mapped = E11y::OpenTelemetry::SemanticConventions.map(event_data[:event_name].to_s, payload)
68
+ mapped.each do |key, value|
69
+ next if value.nil?
70
+
71
+ span.set_attribute(key.to_s, otel_value(value))
72
+ rescue ArgumentError, TypeError
73
+ span.set_attribute(key.to_s, value.to_s)
74
+ end
75
+ end
76
+
77
+ def otel_value(value)
78
+ case value
79
+ when TrueClass, FalseClass, Integer, Float, String then value
80
+ when Array then value.map(&:to_s)
81
+ else value.to_s # Symbol, NilClass, Hash, etc.
82
+ end
83
+ end
84
+
85
+ def set_status(span, event_data)
86
+ if event_data[:severity].in?(%i[error fatal])
87
+ msg = event_data.dig(:payload, :error_message) ||
88
+ event_data.dig(:payload, "error_message") || "Error"
89
+ span.status = ::OpenTelemetry::Trace::Status.error(msg.to_s)
90
+ else
91
+ span.status = ::OpenTelemetry::Trace::Status.ok
92
+ end
93
+ end
94
+
95
+ def record_exception(span, event_data)
96
+ exc = event_data[:exception] || event_data.dig(:payload, :exception) || event_data.dig(:payload, "exception")
97
+ span.record_exception(exc) if exc.is_a?(Exception)
98
+ end
99
+
100
+ def compute_end_timestamp(event_data)
101
+ start = event_data[:timestamp] || Time.now
102
+ start_ns = time_to_nano(start)
103
+ if event_data[:duration_ms]
104
+ start_ns + (event_data[:duration_ms].to_f * 1_000_000).to_i
105
+ else
106
+ time_to_nano(Time.now)
107
+ end
108
+ end
109
+
110
+ def should_create_span?(event_data)
111
+ return true if event_data[:severity].in?(%i[error fatal])
112
+
113
+ patterns = E11y.config&.opentelemetry_span_creation_patterns || []
114
+ event_name = event_data[:event_name].to_s
115
+ return false if event_name.empty?
116
+
117
+ patterns.any? { |p| File.fnmatch(p.to_s, event_name) }
118
+ end
119
+
120
+ def span_kind(event_data)
121
+ kind = (event_data[:span_kind] || :internal).to_sym
122
+ case kind
123
+ when :server then ::OpenTelemetry::Trace::SpanKind::SERVER
124
+ when :client then ::OpenTelemetry::Trace::SpanKind::CLIENT
125
+ when :producer then ::OpenTelemetry::Trace::SpanKind::PRODUCER
126
+ when :consumer then ::OpenTelemetry::Trace::SpanKind::CONSUMER
127
+ else ::OpenTelemetry::Trace::SpanKind::INTERNAL
128
+ end
129
+ rescue StandardError
130
+ ::OpenTelemetry::Trace::SpanKind::INTERNAL
131
+ end
132
+
133
+ def time_to_nano(time)
134
+ return (Time.now.to_f * 1_000_000_000).to_i if time.nil?
135
+
136
+ time = Time.parse(time.to_s) if time.is_a?(String)
137
+ (time.to_f * 1_000_000_000).to_i
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -15,7 +15,7 @@ module E11y
15
15
  EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/
16
16
 
17
17
  # Password-like field names
18
- PASSWORD_FIELDS = /password|passwd|pwd|secret|token|api[_-]?key/i
18
+ PASSWORD_FIELDS = /\b(?:password|passwd|pwd|secret|token|api[_-]?key)\b/i
19
19
 
20
20
  # Social Security Number (US format: XXX-XX-XXXX)
21
21
  SSN = /\b\d{3}-\d{2}-\d{4}\b/
@@ -40,6 +40,17 @@ module E11y
40
40
  PHONE
41
41
  ].freeze
42
42
 
43
+ # Patterns applied to STRING VALUES only (excludes PASSWORD_FIELDS).
44
+ # PASSWORD_FIELDS matches field names (password, token, api_key), not values.
45
+ # Applying it to values corrupts legitimate strings like "process_token_renewal_completed".
46
+ VALUE_PATTERNS = [
47
+ EMAIL,
48
+ SSN,
49
+ CREDIT_CARD,
50
+ IPV4,
51
+ PHONE
52
+ ].freeze
53
+
43
54
  # Field name patterns that indicate PII
44
55
  # Used for field-level detection (case-insensitive)
45
56
  FIELD_PATTERNS = {
@@ -12,7 +12,7 @@ module E11y
12
12
  #
13
13
  # builder.use E11y::Middleware::TraceContext
14
14
  # builder.use E11y::Middleware::Validation
15
- # builder.use E11y::Middleware::PIIFiltering
15
+ # builder.use E11y::Middleware::PIIFilter
16
16
  #
17
17
  # builder.validate_zones! # Boot-time validation
18
18
  #
@@ -26,14 +26,14 @@ module E11y
26
26
  # end
27
27
  #
28
28
  # builder.zone(:security) do
29
- # use E11y::Middleware::PIIFiltering
29
+ # use E11y::Middleware::PIIFilter
30
30
  # end
31
31
  #
32
32
  # @see E11y::Middleware::Base
33
33
  # @see ADR-015 §3.4 Middleware Zones & Modification Rules
34
34
  class Builder
35
35
  # Middleware entry: [middleware_class, args, options]
36
- MiddlewareEntry = Struct.new(:middleware_class, :args, :options, keyword_init: true)
36
+ MiddlewareEntry = Struct.new(:middleware_class, :args, :options)
37
37
 
38
38
  # @return [Array<MiddlewareEntry>] Registered middlewares
39
39
  attr_reader :middlewares
@@ -80,7 +80,7 @@ module E11y
80
80
  #
81
81
  # @example
82
82
  # builder.zone(:security) do
83
- # use E11y::Middleware::PIIFiltering
83
+ # use E11y::Middleware::PIIFilter
84
84
  # end
85
85
  #
86
86
  # @see ADR-015 §3.4.2 Middleware Zones
@@ -38,11 +38,13 @@ module E11y
38
38
  module AuditEvent
39
39
  def self.included(base)
40
40
  base.class_eval do
41
- # Audit events will use audit pipeline (Phase 4)
41
+ audit_event true
42
+ contains_pii false # Preserve all data for signing (Tier 1 = skip filtering)
43
+ use_dlq true # Audit events always saved to DLQ (compliance)
42
44
  # Severity is NOT set by preset - user decides based on event criticality
43
45
  end
44
46
 
45
- # Extend class with audit-specific methods
47
+ # Extend class with audit-specific methods (resolve_sample_rate 1.0, resolve_rate_limit nil)
46
48
  base.extend(ClassMethods)
47
49
  end
48
50
 
@@ -59,6 +61,15 @@ module E11y
59
61
  def resolve_sample_rate
60
62
  1.0 # 100% - compliance requirement
61
63
  end
64
+
65
+ # Audit events use routing rules (UC-012), not severity-based adapters.
66
+ # Return [] when no explicit adapters; respect explicit adapters when set.
67
+ def adapters(*list)
68
+ @adapters = list.flatten if list.any?
69
+ return @adapters if @adapters
70
+
71
+ []
72
+ end
62
73
  end
63
74
  end
64
75
  end
data/lib/e11y/railtie.rb CHANGED
@@ -22,7 +22,7 @@ module E11y
22
22
  # # config/initializers/e11y.rb
23
23
  # E11y.configure do |config|
24
24
  # config.service_name = "my-app"
25
- # config.adapters.register :loki, E11y::Adapters::Loki.new(url: ENV['LOKI_URL'])
25
+ # config.adapters[:loki] = E11y::Adapters::Loki.new(url: ENV['LOKI_URL'])
26
26
  # end
27
27
  #
28
28
  # @see ADR-008 §3 (Railtie & Initialization)
@@ -33,6 +33,13 @@ module E11y
33
33
  # including configuration, middleware insertion, instrumentation setup,
34
34
  # and console integration.
35
35
  class Railtie < Rails::Railtie
36
+ # Wire up generators so `rails g e11y:*` commands are discoverable.
37
+ generators do
38
+ require "generators/e11y/install/install_generator"
39
+ require "generators/e11y/event/event_generator"
40
+ require "generators/e11y/grafana_dashboard/grafana_dashboard_generator"
41
+ require "generators/e11y/prometheus_alerts/prometheus_alerts_generator"
42
+ end
36
43
  # Derive service name from Rails application class
37
44
  # @return [String] Service name (e.g., "my_app")
38
45
  def self.derive_service_name
@@ -47,7 +54,8 @@ module E11y
47
54
  E11y.configure do |config|
48
55
  config.environment ||= Rails.env.to_s
49
56
  config.service_name ||= E11y::Railtie.derive_service_name
50
- # Only set enabled if not already configured
57
+ # Enable in dev/prod; disable in test by default — only when still unset (nil).
58
+ # Respects explicit true/false from earlier E11y.configure (e.g. config/application.rb).
51
59
  config.enabled = !Rails.env.test? if config.enabled.nil?
52
60
  end
53
61
  end
@@ -57,10 +65,37 @@ module E11y
57
65
  next unless E11y.config.enabled
58
66
 
59
67
  # Setup instruments (each can be enabled/disabled separately)
60
- E11y::Railtie.setup_rails_instrumentation if E11y.config.rails_instrumentation&.enabled
61
- E11y::Railtie.setup_logger_bridge if E11y.config.logger_bridge&.enabled
62
- E11y::Railtie.setup_sidekiq if defined?(::Sidekiq) && E11y.config.sidekiq&.enabled
63
- E11y::Railtie.setup_active_job if defined?(::ActiveJob) && E11y.config.active_job&.enabled
68
+ E11y::Railtie.setup_rails_instrumentation if E11y.config.rails_instrumentation_enabled
69
+ E11y::Railtie.setup_logger_bridge if E11y.config.logger_bridge_enabled
70
+ E11y::Railtie.setup_sidekiq if defined?(::Sidekiq) && E11y.config.sidekiq_enabled
71
+ E11y::Railtie.setup_active_job if defined?(::ActiveJob) && E11y.config.active_job_enabled
72
+ end
73
+
74
+ # Outgoing HTTP trace propagation (UC-009)
75
+ initializer "e11y.http_tracing", after: :load_config_initializers do
76
+ next unless E11y.configuration.enable_http_tracing
77
+
78
+ E11y::Tracing.patch_net_http!
79
+ end
80
+
81
+ # Auto-register DevLog adapter in development and test environments.
82
+ # Skipped if the user has already registered :dev_log in their initializer.
83
+ initializer "e11y.setup_development", after: :load_config_initializers do |app|
84
+ next unless Rails.env.development? || Rails.env.test?
85
+ next if E11y.configuration.adapters.key?(:dev_log)
86
+
87
+ E11y.configure do |config|
88
+ config.register_adapter :dev_log, E11y::Adapters::DevLog.new(
89
+ path: Rails.root.join("log", "e11y_dev.jsonl"),
90
+ max_lines: ENV.fetch("E11Y_MAX_EVENTS", "10000").to_i,
91
+ max_size: ENV.fetch("E11Y_MAX_SIZE", "50").to_i * 1024 * 1024,
92
+ keep_rotated: ENV.fetch("E11Y_KEEP_ROTATED", "5").to_i,
93
+ enable_watcher: !Rails.env.test?
94
+ )
95
+ end
96
+
97
+ require "e11y/middleware/dev_log_source"
98
+ app.middleware.use E11y::Middleware::DevLogSource
64
99
  end
65
100
 
66
101
  # Middleware insertion
@@ -69,10 +104,13 @@ module E11y
69
104
 
70
105
  # Insert E11y request middleware before Rails logger
71
106
  # This ensures trace context is set up before any Rails logging
72
- app.middleware.insert_before(
73
- Rails::Rack::Logger,
74
- E11y::Middleware::Request
75
- )
107
+ # API-only mode may omit Rails::Rack::Logger — fall back to unshift
108
+ begin
109
+ app.middleware.insert_before(Rails::Rack::Logger, E11y::Middleware::Request)
110
+ rescue RuntimeError
111
+ # Rails::Rack::Logger not in stack (e.g. api_only)
112
+ app.middleware.unshift(E11y::Middleware::Request)
113
+ end
76
114
  end
77
115
 
78
116
  # Console helpers
@@ -87,10 +125,10 @@ module E11y
87
125
 
88
126
  # Rake task helpers
89
127
  rake_tasks do
90
- next unless E11y.config.enabled
91
-
92
- # TODO: Add rake tasks (e11y:stats, e11y:test_event, etc.)
93
- # load 'e11y/tasks.rake'
128
+ load File.expand_path("../tasks/e11y_slo.rake", __dir__)
129
+ load File.expand_path("../tasks/e11y_lint.rake", __dir__)
130
+ load File.expand_path("../tasks/e11y_events.rake", __dir__)
131
+ load File.expand_path("../tasks/e11y_docs.rake", __dir__)
94
132
  end
95
133
 
96
134
  # Setup Rails instrumentation (ActiveSupport::Notifications → E11y)