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
data/lib/e11y.rb CHANGED
@@ -7,14 +7,27 @@ require "active_support/core_ext/numeric/time" # For 30.days, 7.years, etc.
7
7
  loader = Zeitwerk::Loader.for_gem
8
8
  # Configure inflector for acronyms
9
9
  loader.inflector.inflect(
10
+ "documentation" => "Documentation",
11
+ "debug" => "Debug",
12
+ "opentelemetry_collector" => "OpenTelemetryCollector",
13
+ "otel_span" => "OtelSpan",
10
14
  "pii" => "PII",
11
15
  "pii_filter" => "PIIFilter",
12
16
  "otel_logs" => "OTelLogs",
13
17
  "slo" => "SLO",
14
- "dlq" => "DLQ"
18
+ "dlq" => "DLQ",
19
+ "net_http_patch" => "NetHTTPPatch",
20
+ "rspec_matchers" => "RSpecMatchers",
21
+ "have_tracked_event_matcher" => "HaveTrackedEventMatcher",
22
+ "snapshot_matcher" => "SnapshotMatcher"
15
23
  )
16
24
  # Don't autoload railtie - it will be required manually when Rails is available
17
25
  loader.do_not_eager_load("#{__dir__}/e11y/railtie.rb")
26
+ # Generators live under lib/generators/ — not part of the autoloaded tree
27
+ loader.ignore("#{__dir__}/generators")
28
+ # Optional HTTP tracing files require external gems (faraday, net/http) — loaded on demand only
29
+ loader.ignore("#{__dir__}/e11y/tracing/faraday_middleware.rb")
30
+ loader.ignore("#{__dir__}/e11y/tracing/net_http_patch.rb")
18
31
  loader.setup
19
32
 
20
33
  # E11y - Event-Driven Observability for Ruby on Rails
@@ -24,8 +37,6 @@ loader.setup
24
37
  # config.adapters = [:loki, :sentry]
25
38
  # end
26
39
  #
27
- # E11y.track(Events::UserSignup.new(user_id: 123))
28
- #
29
40
  # @see https://e11y.dev Documentation
30
41
  module E11y
31
42
  class Error < StandardError; end
@@ -33,6 +44,9 @@ module E11y
33
44
  class ZoneViolationError < Error; end
34
45
  class InvalidPipelineError < Error; end
35
46
 
47
+ # Raised when PII key is blocked in baggage (ADR-006 §5.5). Used by BaggageProtection and E11y::Current.add_baggage.
48
+ class BaggagePiiError < Error; end
49
+
36
50
  class << self
37
51
  # Configure E11y
38
52
  #
@@ -56,301 +70,144 @@ module E11y
56
70
  end
57
71
  alias config configuration
58
72
 
59
- # Track an event
73
+ # Test adapter for specs (InMemoryTest in unit tests, InMemory in integration).
74
+ # Returns :test adapter (unit tests) or :memory adapter (integration tests from dummy config).
60
75
  #
61
- # @param event [Event] event instance to track
62
- # @return [void]
76
+ # @return [E11y::Adapters::InMemory, E11y::Adapters::InMemoryTest, nil]
77
+ def test_adapter
78
+ configuration.adapters[:test] || configuration.adapters[:memory]
79
+ end
80
+
81
+ # Trace an event through the pipeline (debug utility).
82
+ # Delegates to PipelineInspector.trace_event. Loads the inspector on demand.
83
+ #
84
+ # @param event_class [Class] event class (e.g., Events::OrderCreated)
85
+ # @param payload [Hash] keyword arguments for the event payload
86
+ # @return [Hash] event_data after pipeline
63
87
  #
64
88
  # @example
65
- # E11y.track(Events::UserSignup.new(user_id: 123))
66
- def track(event)
67
- # TODO: Implement in Phase 1
68
- raise NotImplementedError, "E11y.track will be implemented in Phase 1"
89
+ # E11y.trace(Events::OrderCreated, order_id: "123", amount: 99.99)
90
+ def trace(event_class, **payload)
91
+ require "e11y/debug/pipeline_inspector"
92
+ E11y::Debug::PipelineInspector.trace_event(event_class, **payload)
69
93
  end
70
94
 
71
- # Get logger instance
95
+ # Get logger instance.
96
+ # Priority: config.logger > Rails.logger (when in Rails) > $stdout.
97
+ # Set config.logger = Logger.new(nil) in tests to suppress output.
72
98
  #
73
99
  # @return [Logger] logger instance
74
100
  def logger
101
+ return configuration.logger if configuration&.logger
102
+
103
+ return @logger if defined?(@logger) && !@logger.nil?
104
+
75
105
  require "logger"
76
- @logger ||= ::Logger.new($stdout)
106
+ @logger = if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
107
+ Rails.logger
108
+ else
109
+ ::Logger.new($stdout)
110
+ end
77
111
  end
78
112
 
79
- # Reset configuration (primarily for testing)
113
+ # Initialize E11y and all configured adapters.
114
+ # Call after the configure block at application startup.
80
115
  #
81
116
  # @return [void]
82
- # @api private
83
- def reset!
84
- @configuration = nil
85
- @logger = nil
86
- end
87
- end
88
-
89
- # Configuration class for E11y
90
- #
91
- # Adapters are referenced by name (e.g., :logs, :errors_tracker).
92
- # The actual implementation (Loki, Sentry, etc.) is configured separately.
93
- #
94
- # @example Configure adapters
95
- # E11y.configure do |config|
96
- # # Register adapter instances
97
- # config.adapters[:logs] = E11y::Adapters::Loki.new(url: "...")
98
- # config.adapters[:errors_tracker] = E11y::Adapters::Sentry.new(dsn: "...")
99
- # end
100
- #
101
- # @example Configure severity => adapter mapping
102
- # E11y.configure do |config|
103
- # config.adapter_mapping[:error] = [:logs, :errors_tracker]
104
- # config.adapter_mapping[:info] = [:logs]
105
- # end
106
- #
107
- # @example Configure middleware pipeline
108
- # E11y.configure do |config|
109
- # config.pipeline.use E11y::Middleware::Sampling, default_sample_rate: 0.1
110
- # end
111
- class Configuration
112
- attr_accessor :adapters, :log_level, :enabled, :environment, :service_name, :default_retention_period,
113
- :routing_rules, :fallback_adapters
114
- attr_reader :adapter_mapping, :pipeline, :rails_instrumentation, :logger_bridge, :request_buffer, :active_job,
115
- :sidekiq, :error_handling, :dlq_storage, :dlq_filter, :rate_limiting, :slo_tracking
116
-
117
- def initialize
118
- initialize_basic_config
119
- initialize_routing_config
120
- initialize_feature_configs
121
- configure_default_pipeline
122
- end
123
-
124
- private
117
+ def start!
118
+ return unless configuration.enabled
125
119
 
126
- def initialize_basic_config
127
- @adapters = {} # Hash of adapter_name => adapter_instance
128
- @log_level = :info
129
- @pipeline = E11y::Pipeline::Builder.new
130
- @enabled = true
131
- @environment = nil
132
- @service_name = nil
120
+ configuration.adapters.each_value do |adapter|
121
+ adapter.start! if adapter.respond_to?(:start!)
122
+ end
123
+ logger.info("[E11y] Started (#{configuration.adapters.size} adapters)")
133
124
  end
134
125
 
135
- def initialize_routing_config
136
- @adapter_mapping = default_adapter_mapping
137
- @default_retention_period = 30.days # Default: 30 days retention
138
- @routing_rules = [] # Array of lambdas for retention-based routing
139
- @fallback_adapters = [:stdout] # Fallback if no routing rule matches
140
- end
126
+ # Gracefully shut down E11y, flushing pending events.
127
+ #
128
+ # @param timeout [Integer] Seconds to wait for each adapter flush (default: 5)
129
+ # @return [void]
130
+ def stop!(timeout: 5)
131
+ require "timeout"
132
+ configuration.adapters.each_value do |adapter|
133
+ if adapter.respond_to?(:stop!)
134
+ adapter.stop!(timeout: timeout)
135
+ elsif adapter.respond_to?(:flush!)
136
+ Timeout.timeout(timeout) { adapter.flush! }
137
+ end
138
+ rescue StandardError => e
139
+ logger.warn("[E11y] Adapter stop error: #{e.message}")
140
+ end
141
+ logger.info("[E11y] Stopped")
142
+ end
143
+
144
+ # Check whether E11y will process events with the given severity.
145
+ # Returns false if no healthy adapter is registered for that severity.
146
+ #
147
+ # @param severity [Symbol] e.g. :debug, :info, :error
148
+ # @return [Boolean]
149
+ def enabled_for?(severity)
150
+ return false unless configuration.enabled
141
151
 
142
- def initialize_feature_configs
143
- @rails_instrumentation = RailsInstrumentationConfig.new
144
- @logger_bridge = LoggerBridgeConfig.new
145
- @request_buffer = RequestBufferConfig.new
146
- @active_job = ActiveJobConfig.new
147
- @sidekiq = SidekiqConfig.new
148
- @error_handling = ErrorHandlingConfig.new # ✅ C18 Resolution
149
- @dlq_storage = nil # Set by user (e.g., DLQ::FileStorage instance)
150
- @dlq_filter = nil # Set by user (e.g., DLQ::Filter instance)
151
- @rate_limiting = RateLimitingConfig.new
152
- @slo_tracking = SLOTrackingConfig.new # ✅ L3.14.1
152
+ configuration.adapters_for_severity(severity).any? do |name|
153
+ configuration.adapters[name]&.healthy?
154
+ end
155
+ rescue StandardError
156
+ false
153
157
  end
154
158
 
155
- public
156
-
157
- # Get adapters for given severity
159
+ # Current size of the request-scoped debug buffer for this thread.
158
160
  #
159
- # @param severity [Symbol] Severity level
160
- # @return [Array<Symbol>] Adapter names (e.g., [:logs, :errors_tracker])
161
- def adapters_for_severity(severity)
162
- @adapter_mapping[severity] || @adapter_mapping[:default] || []
161
+ # @return [Integer]
162
+ def buffer_size
163
+ buffer = Thread.current[:e11y_ephemeral_buffer]
164
+ buffer.respond_to?(:size) ? buffer.size : 0
163
165
  end
164
166
 
165
- # Get built pipeline (cached after first call)
167
+ # Circuit breaker states for all adapters.
166
168
  #
167
- # @return [#call] Built middleware pipeline
168
- def built_pipeline
169
- @built_pipeline ||= @pipeline.build(->(_event_data) {})
170
- end
171
-
172
- private
173
-
174
- # Default adapter mapping (convention-based)
169
+ # @return [Hash{Symbol => Symbol}] adapter_name => :closed / :open / :half_open
170
+ def circuit_breaker_state
171
+ configuration.adapters.transform_values do |adapter|
172
+ if adapter.respond_to?(:circuit_breaker_state)
173
+ adapter.circuit_breaker_state
174
+ else
175
+ :closed
176
+ end
177
+ end
178
+ end
179
+
180
+ # Access the global Event Registry singleton.
175
181
  #
176
- # Adapter names represent PURPOSE, not implementation:
177
- # - :logs centralized logging (implementation: Loki, Elasticsearch, CloudWatch, etc.)
178
- # - :errors_tracker → error tracking with alerting (implementation: Sentry, Rollbar, Bugsnag, etc.)
182
+ # The registry auto-populates as event classes are defined (via the `event_name` DSL setter).
183
+ # Useful for introspection, documentation generation, and admin dashboards.
179
184
  #
180
- # @return [Hash{Symbol => Array<Symbol>}] Default mapping (severity => adapter names)
181
- def default_adapter_mapping
182
- {
183
- error: %i[logs errors_tracker], # Errors: both logging + alerting
184
- fatal: %i[logs errors_tracker], # Fatal: both logging + alerting
185
- default: [:logs] # Others: logging only
186
- }
187
- end
188
-
189
- # Setup default middleware pipeline
185
+ # @return [E11y::Registry]
190
186
  #
191
- # Default pipeline order (per ADR-015):
192
- # 1. TraceContext - Add trace_id, span_id, timestamp (zone: :pre_processing)
193
- # 2. Validation - Schema validation (zone: :pre_processing)
194
- # 3. PIIFilter - PII filtering (zone: :security)
195
- # 4. AuditSigning - Audit event signing (zone: :security)
196
- # 5. Sampling - Adaptive sampling (zone: :routing)
197
- # 6. Routing - Buffer routing (zone: :adapters)
198
- #
199
- # @return [void]
200
- # @see ADR-015 Middleware Execution Order
201
- def configure_default_pipeline
202
- # Zone: :pre_processing
203
- @pipeline.use E11y::Middleware::TraceContext
204
- @pipeline.use E11y::Middleware::Validation
205
-
206
- # Zone: :security
207
- @pipeline.use E11y::Middleware::PIIFilter
208
- @pipeline.use E11y::Middleware::AuditSigning
209
-
210
- # Zone: :routing
211
- @pipeline.use E11y::Middleware::Sampling
212
-
213
- # Zone: :adapters
214
- @pipeline.use E11y::Middleware::Routing
215
- end
216
- end
217
-
218
- # Rails Instrumentation configuration
219
- class RailsInstrumentationConfig
220
- attr_accessor :enabled, :custom_mappings, :ignore_events
221
-
222
- def initialize
223
- @enabled = false # Disabled by default, enabled by Railtie
224
- @custom_mappings = {}
225
- @ignore_events = []
226
- end
227
-
228
- # Override event class for specific ASN pattern (Devise-style)
229
- # @param pattern [String] ActiveSupport::Notifications pattern
230
- # @param event_class [Class] E11y event class
231
- # @return [void]
232
- def event_class_for(pattern, event_class)
233
- @custom_mappings[pattern] = event_class
187
+ # @example
188
+ # E11y.registry.event_classes
189
+ # E11y.registry.find("order.created")
190
+ def registry
191
+ Registry.instance
234
192
  end
235
193
 
236
- # Ignore specific ASN event
237
- # @param pattern [String] ActiveSupport::Notifications pattern
194
+ # Reset configuration (primarily for testing)
195
+ #
238
196
  # @return [void]
239
- def ignore_event(pattern)
240
- @ignore_events << pattern
241
- end
242
- end
243
-
244
- # Logger Bridge configuration
245
- #
246
- # Controls Rails.logger integration:
247
- # - When enabled = true: wraps Rails.logger and sends logs to E11y
248
- # - When enabled = false: no integration (default)
249
- #
250
- # @example Enable logger bridge
251
- # E11y.configure do |config|
252
- # config.logger_bridge.enabled = true # Wrap Rails.logger + send to E11y
253
- # end
254
- #
255
- # @see lib/e11y/logger/bridge.rb
256
- class LoggerBridgeConfig
257
- attr_accessor :enabled
258
-
259
- def initialize
260
- @enabled = false # Opt-in: disabled by default
261
- end
262
- end
263
-
264
- # Request Buffer configuration
265
- class RequestBufferConfig
266
- attr_accessor :enabled
267
-
268
- def initialize
269
- @enabled = false # Disabled by default
270
- end
271
- end
272
-
273
- # ActiveJob configuration
274
- #
275
- # Controls ActiveJob integration (callbacks for event tracking).
276
- # When enabled, E11y will automatically track job lifecycle events:
277
- # - job.enqueued
278
- # - job.started
279
- # - job.completed
280
- # - job.failed
281
- #
282
- # @see lib/e11y/instruments/active_job.rb
283
- class ActiveJobConfig
284
- attr_accessor :enabled
285
-
286
- def initialize
287
- @enabled = false # Disabled by default, enabled by Railtie
288
- end
289
- end
290
-
291
- # Sidekiq configuration
292
- #
293
- # Controls Sidekiq middleware integration for trace propagation and context setup.
294
- # Automatically enabled by Railtie when Sidekiq is detected.
295
- #
296
- # @see ADR-008 §9 (Sidekiq Integration)
297
- class SidekiqConfig
298
- attr_accessor :enabled
299
-
300
- def initialize
301
- @enabled = false # Disabled by default, enabled by Railtie when Sidekiq is present
302
- end
303
- end
304
-
305
- # Error Handling configuration (C18 Resolution)
306
- #
307
- # Controls whether event tracking failures should raise exceptions.
308
- # Default: true (for web requests - fast feedback)
309
- # Exception: false (for background jobs - don't fail business logic)
310
- #
311
- # @see ADR-013 §3.6 (Event Tracking in Background Jobs)
312
- class ErrorHandlingConfig
313
- attr_accessor :fail_on_error
314
-
315
- def initialize
316
- @fail_on_error = true # Default: raise errors (fast feedback for web requests)
317
- end
318
- end
319
-
320
- # Rate Limiting configuration (UC-011, C02 Resolution)
321
- #
322
- # Protects adapters from event floods using token bucket algorithm.
323
- #
324
- # @see UC-011 (Rate Limiting - DoS Protection)
325
- # @see ADR-013 §4.6 (C02 Resolution)
326
- class RateLimitingConfig
327
- attr_accessor :enabled, :global_limit, :per_event_limit, :window
328
-
329
- def initialize
330
- @enabled = false # Opt-in (enable explicitly)
331
- @global_limit = 10_000 # Max 10K events/sec globally
332
- @per_event_limit = 1_000 # Max 1K events/sec per event type
333
- @window = 1.0 # 1 second window
197
+ # @api private
198
+ def reset!
199
+ @configuration = nil
200
+ @logger = nil
201
+ E11y::Metrics.reset_backend!
334
202
  end
335
203
  end
336
204
 
337
- # SLO Tracking configuration (UC-004, ADR-003)
338
- #
339
- # Zero-config SLO tracking for HTTP requests and background jobs.
340
- # Automatically emits SLO metrics (availability, latency, success rate).
341
- #
342
- # @see UC-004 (Zero-Config SLO Tracking)
343
- # @see ADR-003 (SLO & Observability)
344
- #
345
- # @note C11 Resolution (Sampling Correction): Requires Phase 2.8 (Stratified Sampling).
346
- # Without stratified sampling, SLO metrics may be inaccurate when adaptive sampling is enabled.
347
- class SLOTrackingConfig
348
- attr_accessor :enabled
349
-
350
- def initialize
351
- @enabled = false # Opt-in (enable explicitly)
352
- end
353
- end
205
+ # Default allowed keys for baggage protection (ADR-006 §5.5).
206
+ # Used when security_baggage_protection_allowed_keys is not set.
207
+ BAGGAGE_PROTECTION_DEFAULT_ALLOWED_KEYS = %w[
208
+ trace_id span_id environment version service_name deployment_id request_id
209
+ user_id experiment experiment_id tenant feature_flag
210
+ ].freeze
354
211
  end
355
212
 
356
213
  # Load Railtie if Rails is present
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module E11y
6
+ module Generators
7
+ # Generates an event class under app/events/.
8
+ #
9
+ # @example
10
+ # rails g e11y:event OrderPaid
11
+ # # => creates app/events/events/order_paid.rb
12
+ class EventGenerator < Rails::Generators::NamedBase
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ desc "Creates an E11y event class in app/events/."
16
+
17
+ def create_event_file
18
+ template "event.rb.tt", File.join("app/events/events", "#{file_name}.rb")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Events
4
+ class <%= class_name %> < E11y::Event::Base
5
+ # severity :info # auto-inferred from class name; override if needed
6
+
7
+ schema do
8
+ # required(:field_name).filled(:string)
9
+ # optional(:other_field).maybe(:integer)
10
+ end
11
+
12
+ # metrics do
13
+ # counter :<%= file_name.gsub("/", "_") %>_total
14
+ # end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module E11y
6
+ module Generators
7
+ # Generates a Grafana dashboard JSON for E11y metrics.
8
+ #
9
+ # Requires Yabeda/Prometheus integration.
10
+ #
11
+ # @example
12
+ # rails g e11y:grafana_dashboard
13
+ # # => creates config/grafana/e11y_dashboard.json
14
+ class GrafanaDashboardGenerator < Rails::Generators::Base
15
+ source_root File.expand_path("templates", __dir__)
16
+
17
+ desc "Creates a Grafana dashboard JSON for E11y metrics in config/grafana/."
18
+
19
+ def create_dashboard
20
+ empty_directory "config/grafana"
21
+ template "e11y_dashboard.json", "config/grafana/e11y_dashboard.json"
22
+ end
23
+
24
+ def show_readme
25
+ say "\n✅ Grafana dashboard created: config/grafana/e11y_dashboard.json", :green
26
+ say " Import it via Grafana → Dashboards → Import → Upload JSON file\n"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,81 @@
1
+ {
2
+ "title": "E11y Observability",
3
+ "uid": "e11y-overview",
4
+ "version": 1,
5
+ "schemaVersion": 36,
6
+ "tags": ["e11y", "ruby", "observability"],
7
+ "panels": [
8
+ {
9
+ "id": 1,
10
+ "type": "stat",
11
+ "title": "Events / sec",
12
+ "targets": [
13
+ {
14
+ "expr": "rate(e11y_events_total[1m])",
15
+ "legendFormat": "{{event_name}}"
16
+ }
17
+ ],
18
+ "gridPos": { "x": 0, "y": 0, "w": 6, "h": 4 }
19
+ },
20
+ {
21
+ "id": 2,
22
+ "type": "stat",
23
+ "title": "Error rate",
24
+ "targets": [
25
+ {
26
+ "expr": "rate(e11y_events_total{severity=\"error\"}[1m]) / rate(e11y_events_total[1m])",
27
+ "legendFormat": "error rate"
28
+ }
29
+ ],
30
+ "gridPos": { "x": 6, "y": 0, "w": 6, "h": 4 }
31
+ },
32
+ {
33
+ "id": 3,
34
+ "type": "timeseries",
35
+ "title": "Events by severity",
36
+ "targets": [
37
+ {
38
+ "expr": "rate(e11y_events_total[1m])",
39
+ "legendFormat": "{{severity}}"
40
+ }
41
+ ],
42
+ "gridPos": { "x": 0, "y": 4, "w": 12, "h": 8 }
43
+ },
44
+ {
45
+ "id": 4,
46
+ "type": "stat",
47
+ "title": "Rate limit drops",
48
+ "targets": [
49
+ {
50
+ "expr": "rate(e11y_rate_limit_dropped_total[1m])",
51
+ "legendFormat": "dropped"
52
+ }
53
+ ],
54
+ "gridPos": { "x": 12, "y": 0, "w": 6, "h": 4 }
55
+ },
56
+ {
57
+ "id": 5,
58
+ "type": "stat",
59
+ "title": "Circuit breaker trips",
60
+ "targets": [
61
+ {
62
+ "expr": "e11y_circuit_breaker_transitions_total{event=\"opened\"}",
63
+ "legendFormat": "{{adapter}}"
64
+ }
65
+ ],
66
+ "gridPos": { "x": 18, "y": 0, "w": 6, "h": 4 }
67
+ },
68
+ {
69
+ "id": 6,
70
+ "type": "timeseries",
71
+ "title": "DLQ queue depth",
72
+ "targets": [
73
+ {
74
+ "expr": "e11y_dlq_size",
75
+ "legendFormat": "DLQ"
76
+ }
77
+ ],
78
+ "gridPos": { "x": 12, "y": 4, "w": 12, "h": 8 }
79
+ }
80
+ ]
81
+ }
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module E11y
6
+ module Generators
7
+ # Creates config/initializers/e11y.rb and app/events/ directory scaffold.
8
+ #
9
+ # @example
10
+ # rails g e11y:install
11
+ class InstallGenerator < Rails::Generators::Base
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ desc "Creates an E11y initializer and the app/events/ directory."
15
+
16
+ def create_initializer
17
+ template "e11y.rb", "config/initializers/e11y.rb"
18
+ end
19
+
20
+ def create_events_directory
21
+ empty_directory "app/events"
22
+ end
23
+
24
+ def show_readme
25
+ say "\n✅ E11y installed!", :green
26
+ say " • config/initializers/e11y.rb — configure adapters here"
27
+ say " • app/events/ — put your event classes here"
28
+ say "\nNext steps:"
29
+ say " rails g e11y:event OrderPaid # generate an event class"
30
+ say " E11y.start! # call after configure in production\n"
31
+ end
32
+ end
33
+ end
34
+ end