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
@@ -10,15 +10,14 @@ module E11y
10
10
  # **Unidirectional Flow:** ASN → E11y
11
11
  #
12
12
  # @example Basic usage
13
- # # Automatically enabled by E11y::Railtie if config.rails_instrumentation.enabled = true
13
+ # # Automatically enabled by E11y::Railtie if config.rails_instrumentation_enabled = true
14
14
  # E11y::Instruments::RailsInstrumentation.setup!
15
15
  #
16
16
  # @example Custom event mapping
17
17
  # E11y.configure do |config|
18
- # config.rails_instrumentation do
19
- # event_class_for 'sql.active_record', MyApp::CustomQueryEvent
20
- # ignore_event 'cache_read.active_support'
21
- # end
18
+ # config.rails_instrumentation_enabled = true
19
+ # config.rails_instrumentation_custom_mappings['sql.active_record'] = MyApp::CustomQueryEvent
20
+ # config.rails_instrumentation_ignore_events << 'cache_read.active_support'
22
21
  # end
23
22
  #
24
23
  # @see ADR-008 §4.1 (Unidirectional Flow ASN → E11y)
@@ -41,6 +40,7 @@ module E11y
41
40
  "enqueue.active_job" => "E11y::Events::Rails::Job::Enqueued",
42
41
  "enqueue_at.active_job" => "E11y::Events::Rails::Job::Scheduled",
43
42
  "perform_start.active_job" => "E11y::Events::Rails::Job::Started",
43
+ # perform.active_job: Completed on success, Failed on exception (routed in track_rails_event)
44
44
  "perform.active_job" => "E11y::Events::Rails::Job::Completed"
45
45
  }.freeze
46
46
 
@@ -50,7 +50,7 @@ module E11y
50
50
  #
51
51
  # @return [void]
52
52
  def self.setup!
53
- return unless E11y.config.rails_instrumentation&.enabled
53
+ return unless E11y.config.rails_instrumentation_enabled
54
54
 
55
55
  # Subscribe to each configured event pattern
56
56
  event_mapping.each do |asn_pattern, e11y_event_class_name|
@@ -81,25 +81,50 @@ module E11y
81
81
  # # Result: { controller: "Users", action: "index" } - password filtered by schema
82
82
  def self.subscribe_to_event(asn_pattern, e11y_event_class_name)
83
83
  ActiveSupport::Notifications.subscribe(asn_pattern) do |name, start, finish, _id, payload|
84
- # Convert ASN event → E11y event
85
- duration = (finish - start) * 1000 # Convert to milliseconds
84
+ track_rails_event(name, start, finish, payload, e11y_event_class_name)
85
+ rescue StandardError => e
86
+ warn "[E11y] Failed to track Rails event #{name}: #{e.message}"
87
+ end
88
+ end
86
89
 
87
- # Resolve event class (string → constant)
90
+ def self.track_rails_event(name, start, finish, payload, e11y_event_class_name)
91
+ duration = (finish - start) * 1000
92
+ extracted_payload = extract_job_info_from_object(payload)
93
+
94
+ # perform.active_job: route to Failed when job raised exception
95
+ if name == "perform.active_job" && job_failed?(payload)
96
+ e11y_event_class = resolve_event_class("E11y::Events::Rails::Job::Failed")
97
+ extracted_payload = extracted_payload.merge(extract_job_exception_info(payload))
98
+ else
88
99
  e11y_event_class = resolve_event_class(e11y_event_class_name)
89
- next unless e11y_event_class
100
+ extracted_payload = extracted_payload.merge(severity: :error) if process_action_error?(name, payload)
101
+ end
90
102
 
91
- # Extract job info from job object if present (ActiveJob events)
92
- extracted_payload = extract_job_info_from_object(payload)
103
+ return unless e11y_event_class
93
104
 
94
- # Track E11y event - schema will filter relevant fields
95
- e11y_event_class.track(
96
- event_name: name,
97
- duration: duration,
98
- **extracted_payload # Pass all payload, schema filters
99
- )
100
- rescue StandardError => e
101
- # Don't crash the app if event tracking fails
102
- warn "[E11y] Failed to track Rails event #{name}: #{e.message}"
105
+ e11y_event_class.track(event_name: name, duration: duration, **extracted_payload)
106
+ end
107
+
108
+ def self.process_action_error?(name, payload)
109
+ name == "process_action.action_controller" && (payload[:exception] || payload["exception"])
110
+ end
111
+
112
+ def self.job_failed?(payload)
113
+ payload[:exception].present? || payload["exception"].present?
114
+ end
115
+
116
+ # Extract error_class and error_message from ActiveJob exception payload.
117
+ # Rails passes exception as ["ErrorClass", "message"] or exception_object.
118
+ def self.extract_job_exception_info(payload)
119
+ ex = payload[:exception] || payload["exception"]
120
+ return {} unless ex
121
+
122
+ if ex.is_a?(Array) && ex.size >= 2
123
+ { error_class: ex[0].to_s, error_message: ex[1].to_s }
124
+ elsif ex.respond_to?(:class) && ex.respond_to?(:message)
125
+ { error_class: ex.class.name, error_message: ex.message.to_s }
126
+ else
127
+ {}
103
128
  end
104
129
  end
105
130
 
@@ -110,9 +135,9 @@ module E11y
110
135
  mapping = DEFAULT_RAILS_EVENT_MAPPING.dup
111
136
 
112
137
  # Apply custom mappings from config (Devise-style overrides)
113
- custom_mappings = E11y.config.rails_instrumentation&.custom_mappings || {}
138
+ custom_mappings = E11y.config.rails_instrumentation_custom_mappings || {}
114
139
  custom_mappings.each do |pattern, event_class|
115
- mapping[pattern] = event_class.name
140
+ mapping[pattern] = event_class.respond_to?(:name) ? event_class.name : event_class.to_s
116
141
  end
117
142
 
118
143
  mapping
@@ -123,7 +148,7 @@ module E11y
123
148
  # @param pattern [String] ASN event pattern
124
149
  # @return [Boolean] true if should be ignored
125
150
  def self.ignored?(pattern)
126
- ignore_list = E11y.config.rails_instrumentation&.ignore_events || []
151
+ ignore_list = E11y.config.rails_instrumentation_ignore_events || []
127
152
  ignore_list.include?(pattern)
128
153
  end
129
154
 
@@ -23,62 +23,151 @@ module E11y
23
23
  #
24
24
  # @see ADR-008 §9 (Sidekiq Integration)
25
25
  module Sidekiq
26
+ # Shared helper: detect raw Sidekiq jobs (not ActiveJob-wrapped)
27
+ module RawSidekiqJob
28
+ def raw_sidekiq_job?(job)
29
+ job_class = job["class"].to_s
30
+ return false if job_class.include?("ActiveJob::QueueAdapters::SidekiqAdapter")
31
+ return false if job["wrapped"].present?
32
+
33
+ true
34
+ end
35
+ end
36
+
37
+ # Emits job lifecycle events (Started, Completed, Failed) for ServerMiddleware
38
+ module JobEventEmitter
39
+ def emit_job_started(job, queue)
40
+ Events::Rails::Job::Started.track(
41
+ event_name: "sidekiq.perform_start",
42
+ duration: 0,
43
+ job_class: job["class"],
44
+ job_id: job["jid"],
45
+ queue: queue
46
+ )
47
+ rescue StandardError => e
48
+ warn "[E11y] Failed to emit job Started: #{e.message}"
49
+ end
50
+
51
+ def emit_job_completed(job, queue, start_time)
52
+ duration_ms = ((Time.now - start_time) * 1000).round(2)
53
+ Events::Rails::Job::Completed.track(
54
+ event_name: "sidekiq.perform",
55
+ duration: duration_ms,
56
+ job_class: job["class"],
57
+ job_id: job["jid"],
58
+ queue: queue
59
+ )
60
+ rescue StandardError => e
61
+ warn "[E11y] Failed to emit job Completed: #{e.message}"
62
+ end
63
+
64
+ def emit_job_failed(job, queue, start_time, error)
65
+ duration_ms = ((Time.now - start_time) * 1000).round(2)
66
+ Events::Rails::Job::Failed.track(
67
+ event_name: "sidekiq.perform",
68
+ duration: duration_ms,
69
+ job_class: job["class"],
70
+ job_id: job["jid"],
71
+ queue: queue,
72
+ error_class: error.class.name,
73
+ error_message: error.message
74
+ )
75
+ rescue StandardError => e
76
+ warn "[E11y] Failed to emit job Failed: #{e.message}"
77
+ end
78
+ end
79
+
26
80
  # Client-side middleware: Inject trace context when enqueueing job
27
81
  #
28
82
  # **C17 Hybrid Tracing**: Propagates parent_trace_id to job metadata.
29
83
  # Job will create NEW trace_id but keep link to parent.
84
+ #
85
+ # **Job lifecycle events**: Emits Events::Rails::Job::Enqueued for raw Sidekiq jobs only.
86
+ # ActiveJob jobs are handled by RailsInstrumentation (ASN).
30
87
  class ClientMiddleware
31
- def call(_worker_class, job, _queue, _redis_pool)
88
+ include RawSidekiqJob
89
+
90
+ def call(worker_class, job, queue, _redis_pool)
32
91
  # Inject current trace context into job metadata as parent trace
33
92
  # Job will generate NEW trace_id but keep parent link (C17)
34
93
  job["e11y_parent_trace_id"] = E11y::Current.trace_id if E11y::Current.trace_id
35
94
  job["e11y_parent_span_id"] = E11y::Current.span_id if E11y::Current.span_id
95
+ job["e11y_sampled"] = E11y::Current.sampled if E11y::Current.respond_to?(:sampled) && !E11y::Current.sampled.nil?
96
+ baggage = E11y::Tracing::Propagator.baggage_for_propagation_from_current
97
+ job["e11y_baggage"] = baggage if baggage.any?
98
+
99
+ # Emit Enqueued for raw Sidekiq jobs only (ActiveJob emits via ASN)
100
+ emit_job_enqueued(worker_class, job, queue) if raw_sidekiq_job?(job)
36
101
 
37
102
  yield
38
103
  end
104
+
105
+ private
106
+
107
+ def emit_job_enqueued(worker_class, job, queue)
108
+ Events::Rails::Job::Enqueued.track(
109
+ event_name: "sidekiq.enqueue",
110
+ duration: 0,
111
+ job_class: worker_class.to_s,
112
+ job_id: job["jid"],
113
+ queue: queue
114
+ )
115
+ rescue StandardError => e
116
+ warn "[E11y] Failed to emit job Enqueued: #{e.message}"
117
+ end
39
118
  end
40
119
 
41
120
  # Server-side middleware: Set up job-scoped context when executing job
42
121
  #
43
122
  # **C17 Hybrid Tracing**: Creates NEW trace_id for job, but preserves parent link.
44
123
  # **C18 Non-Failing**: E11y errors don't fail jobs (observability is secondary to business logic).
124
+ #
125
+ # **Job lifecycle events**: Emits Events::Rails::Job::Started/Completed/Failed for raw Sidekiq jobs only.
126
+ # ActiveJob jobs (when Sidekiq is the queue adapter) are handled by RailsInstrumentation (ASN).
45
127
  class ServerMiddleware
46
- # rubocop:disable Metrics/AbcSize
47
- def call(_worker, job, queue)
48
- # C18: Disable fail_on_error for jobs (observability should not block business logic)
49
- original_fail_on_error = E11y.config.error_handling.fail_on_error
50
- E11y.config.error_handling.fail_on_error = false
51
-
52
- setup_job_context(job)
53
- setup_job_buffer
128
+ include RawSidekiqJob
129
+ include JobEventEmitter
54
130
 
55
- # Track job start time for SLO
131
+ def call(_worker, job, queue)
132
+ original_fail_on_error = disable_fail_on_error
56
133
  start_time = Time.now
57
134
  job_status = :success
58
135
 
59
- # Execute job (business logic)
136
+ setup_job_context(job, queue)
137
+ setup_job_buffer
138
+
139
+ emit_job_started(job, queue) if raw_sidekiq_job?(job)
60
140
  yield
61
141
  rescue StandardError => e
62
142
  job_status = :failed
63
- # Check if this is E11y error (circuit breaker, retry exhausted, etc.)
64
- handle_job_error(e)
65
-
66
- raise # Always re-raise original exception
143
+ on_job_exception(job, queue, start_time, e)
144
+ raise
67
145
  ensure
68
- # Track SLO metrics
69
- track_job_slo(job, queue, job_status, start_time)
146
+ finalize_job(job, queue, start_time, job_status, original_fail_on_error)
147
+ end
70
148
 
71
- cleanup_job_context
149
+ private
72
150
 
73
- # Restore original setting
74
- E11y.config.error_handling.fail_on_error = original_fail_on_error
151
+ def disable_fail_on_error
152
+ original = E11y.config.error_handling_fail_on_error
153
+ E11y.config.error_handling_fail_on_error = false
154
+ original
75
155
  end
76
- # rubocop:enable Metrics/AbcSize
77
156
 
78
- private
157
+ def on_job_exception(job, queue, start_time, error)
158
+ emit_job_failed(job, queue, start_time, error) if raw_sidekiq_job?(job)
159
+ handle_job_error(error)
160
+ end
161
+
162
+ def finalize_job(job, queue, start_time, job_status, original_fail_on_error)
163
+ emit_job_completed(job, queue, start_time) if raw_sidekiq_job?(job) && job_status == :success
164
+ track_job_slo(job, queue, job_status, start_time)
165
+ cleanup_job_context
166
+ E11y.config.error_handling_fail_on_error = original_fail_on_error
167
+ end
79
168
 
80
169
  # Setup job-scoped context (C17 Hybrid Tracing)
81
- def setup_job_context(job)
170
+ def setup_job_context(job, queue = nil)
82
171
  # Extract parent trace context from job metadata
83
172
  parent_trace_id = job["e11y_parent_trace_id"]
84
173
 
@@ -91,13 +180,28 @@ module E11y
91
180
  E11y::Current.span_id = span_id
92
181
  E11y::Current.parent_trace_id = parent_trace_id
93
182
  E11y::Current.request_id = job["jid"]
183
+ E11y::Tracing::Propagator.hydrate_current_from_job_baggage!(job["e11y_baggage"]) if job.key?("e11y_baggage")
184
+
185
+ # Restore or compute sampling decision (ADR-005 §7)
186
+ if job.key?("e11y_sampled")
187
+ E11y::Current.sampled = job["e11y_sampled"]
188
+ else
189
+ require "e11y/trace_context/sampler"
190
+ ctx = E11y::Current.to_context.merge(
191
+ job_class: job["class"],
192
+ queue: queue
193
+ ).compact
194
+ E11y::Current.sampled = E11y::TraceContext::Sampler.should_sample?(ctx)
195
+ end
94
196
  end
95
197
 
96
- # Setup job-scoped buffer
198
+ # Setup request-scoped buffer (same as HTTP; optional job_buffer_limit)
97
199
  def setup_job_buffer
98
- return unless E11y.config.request_buffer&.enabled
200
+ return unless E11y.config.ephemeral_buffer_enabled
99
201
 
100
- E11y::Buffers::RequestScopedBuffer.initialize!
202
+ limit = E11y.config.ephemeral_buffer_job_buffer_limit ||
203
+ E11y::Buffers::EphemeralBuffer::DEFAULT_BUFFER_LIMIT
204
+ E11y::Buffers::EphemeralBuffer.initialize!(buffer_limit: limit)
101
205
  rescue StandardError => e
102
206
  # C18: Don't fail job if buffer setup fails
103
207
  warn "[E11y] Failed to start job buffer: #{e.message}"
@@ -106,9 +210,9 @@ module E11y
106
210
  # Handle job error (C18: Non-Failing Event Tracking)
107
211
  def handle_job_error(_error)
108
212
  # Flush buffer on error (includes debug events)
109
- return unless E11y.config.request_buffer&.enabled
213
+ return unless E11y.config.ephemeral_buffer_enabled
110
214
 
111
- E11y::Buffers::RequestScopedBuffer.flush_on_error
215
+ E11y::Buffers::EphemeralBuffer.flush_on_error
112
216
  rescue StandardError => e
113
217
  # C18: Don't fail job if buffer flush fails
114
218
  warn "[E11y] Failed to flush job buffer on error: #{e.message}"
@@ -117,9 +221,9 @@ module E11y
117
221
  # Cleanup job-scoped context
118
222
  def cleanup_job_context
119
223
  # Discard buffer on success (not on error, already flushed in rescue)
120
- if !$ERROR_INFO && E11y.config.request_buffer&.enabled
224
+ if !$ERROR_INFO && E11y.config.ephemeral_buffer_enabled
121
225
  begin
122
- E11y::Buffers::RequestScopedBuffer.discard
226
+ E11y::Buffers::EphemeralBuffer.discard
123
227
  rescue StandardError => e
124
228
  # C18: Don't fail job if buffer flush fails
125
229
  warn "[E11y] Failed to flush job buffer: #{e.message}"
@@ -154,7 +258,7 @@ module E11y
154
258
  # @return [void]
155
259
  # @api private
156
260
  def track_job_slo(job, queue, status, start_time)
157
- return unless E11y.config.slo_tracking&.enabled
261
+ return unless E11y.config.respond_to?(:slo_tracking_enabled) && E11y.config.slo_tracking_enabled
158
262
 
159
263
  duration_ms = ((Time.now - start_time) * 1000).round(2)
160
264
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Linters
5
+ # Namespace for linter base infrastructure (satisfies Zeitwerk for base.rb).
6
+ module Base
7
+ end
8
+
9
+ class LinterError < StandardError; end
10
+ end
11
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/linters/base"
4
+ require "e11y/registry"
5
+
6
+ module E11y
7
+ module Linters
8
+ module PII
9
+ # Linter for explicit PII declaration on Event classes.
10
+ #
11
+ # When an event declares `contains_pii true`, every schema field must have
12
+ # an explicit PII strategy in the pii_filtering block.
13
+ #
14
+ # @see ADR-006 §3.0.5 PII Declaration Linter
15
+ # @see UC-007 PII Filtering
16
+ class PiiDeclarationLinter
17
+ VALID_STRATEGIES = %i[allow skip mask hash redact partial truncate encrypt].freeze
18
+
19
+ class << self
20
+ # Validate all registered event classes.
21
+ #
22
+ # @raise [E11y::Linters::PiiDeclarationError] when any event with contains_pii true has missing/invalid declarations
23
+ def validate_all!
24
+ errors = []
25
+
26
+ E11y::Registry.event_classes.each do |event_class|
27
+ validate!(event_class)
28
+ rescue PiiDeclarationError => e
29
+ errors << e.message
30
+ end
31
+
32
+ raise PiiDeclarationError, errors.join("\n\n") if errors.any?
33
+ end
34
+
35
+ # Validate a single event class.
36
+ #
37
+ # @param event_class [Class] Event class to validate
38
+ # @raise [E11y::Linters::PiiDeclarationError] when validation fails
39
+ def validate!(event_class)
40
+ return unless event_class.contains_pii == true
41
+
42
+ schema_fields = extract_schema_keys(event_class)
43
+ return if schema_fields.nil? || schema_fields.empty?
44
+
45
+ pii_config = event_class.pii_filtering_config
46
+ declared_fields = pii_config&.dig(:fields)&.keys&.map(&:to_s) || []
47
+
48
+ missing = schema_fields.map(&:to_s) - declared_fields
49
+ raise PiiDeclarationError, build_missing_message(event_class, missing) if missing.any?
50
+
51
+ # Validate each declared field has valid strategy
52
+ pii_config[:fields].each do |field, config|
53
+ validate_field_config!(event_class, field, config)
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def extract_schema_keys(klass)
60
+ return nil unless klass.respond_to?(:compiled_schema)
61
+
62
+ schema = klass.compiled_schema
63
+ return nil unless schema.respond_to?(:key_map)
64
+
65
+ schema.key_map.keys.map(&:name)
66
+ rescue StandardError
67
+ nil
68
+ end
69
+
70
+ def build_missing_message(event_class, missing_fields)
71
+ fields_snippet = missing_fields.map do |f|
72
+ " field :#{f} do\n strategy :mask # or :hash, :allow, :redact\n end"
73
+ end.join("\n ")
74
+
75
+ <<~ERROR
76
+ PII Declaration Error: #{event_class.name}
77
+
78
+ Event declared `contains_pii true` but missing field declarations:
79
+
80
+ Missing fields: #{missing_fields.map { |x| ":#{x}" }.join(', ')}
81
+
82
+ Fix: Add explicit PII strategy for each field in pii_filtering block:
83
+
84
+ class #{event_class.name} < E11y::Event::Base
85
+ contains_pii true
86
+
87
+ pii_filtering do
88
+ #{fields_snippet}
89
+ end
90
+ end
91
+
92
+ Available strategies: #{VALID_STRATEGIES.map { |s| ":#{s}" }.join(', ')}
93
+ ERROR
94
+ end
95
+
96
+ def validate_field_config!(event_class, field, config)
97
+ strategy = config[:strategy]
98
+ unless VALID_STRATEGIES.include?(strategy)
99
+ raise PiiDeclarationError, <<~ERROR
100
+ Invalid PII strategy for #{event_class.name}##{field}
101
+
102
+ Strategy: #{strategy.inspect}
103
+ Valid strategies: #{VALID_STRATEGIES.map { |s| ":#{s}" }.join(', ')}
104
+ ERROR
105
+ end
106
+
107
+ return unless config.key?(:exclude_adapters)
108
+
109
+ return if config[:exclude_adapters].is_a?(Array)
110
+
111
+ raise PiiDeclarationError, "exclude_adapters must be an Array for #{event_class.name}##{field}"
112
+ end
113
+ end
114
+ end
115
+
116
+ # Raised when PII declaration validation fails.
117
+ class PiiDeclarationError < LinterError; end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/linters/base"
4
+ require "e11y/slo/config_loader"
5
+
6
+ module E11y
7
+ module Linters
8
+ module SLO
9
+ # Linter for slo.yml custom_slos consistency with Event class definitions.
10
+ #
11
+ # Ensures every event referenced in slo.yml custom_slos:
12
+ # - Exists (constantize succeeds)
13
+ # - Has slo enabled (slo_enabled?)
14
+ # - Has contributes_to matching the slo_name in config
15
+ class ConfigConsistencyLinter
16
+ class << self
17
+ # Validate slo.yml custom_slos against Event class definitions.
18
+ #
19
+ # @param search_paths [Array<String>, nil] Optional search paths for ConfigLoader.
20
+ # When nil, ConfigLoader uses default paths.
21
+ # @raise [E11y::Linters::LinterError] when any event fails validation
22
+ def validate!(search_paths: nil)
23
+ config = if search_paths
24
+ E11y::SLO::ConfigLoader.load(search_paths: search_paths)
25
+ else
26
+ E11y::SLO::ConfigLoader.load
27
+ end
28
+
29
+ return if config.nil?
30
+ return if config["custom_slos"].nil? || config["custom_slos"].empty?
31
+
32
+ errors = []
33
+
34
+ config["custom_slos"].each do |slo|
35
+ slo_name = slo["name"]
36
+ events = slo["events"] || []
37
+
38
+ events.each do |event_class_name|
39
+ error = validate_event(slo_name, event_class_name)
40
+ errors << error if error
41
+ end
42
+ end
43
+
44
+ return if errors.empty?
45
+
46
+ raise LinterError, errors.join("\n")
47
+ end
48
+
49
+ private
50
+
51
+ def validate_event(slo_name, event_class_name)
52
+ event_class = constantize_event(event_class_name)
53
+ return "Event class '#{event_class_name}' does not exist (constantize failed)" if event_class.nil?
54
+
55
+ unless event_class.respond_to?(:slo_enabled?) && event_class.slo_enabled?
56
+ return "Event #{event_class_name} is referenced in slo.yml (SLO '#{slo_name}') but has slo disabled"
57
+ end
58
+
59
+ contributes_to = event_class.slo_config&.contributes_to_value
60
+ unless contributes_to == slo_name
61
+ return "Event #{event_class_name} contributes_to '#{contributes_to}' but slo.yml defines SLO '#{slo_name}'"
62
+ end
63
+
64
+ nil
65
+ end
66
+
67
+ def constantize_event(event_class_name)
68
+ Object.const_get(event_class_name)
69
+ rescue NameError
70
+ nil
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/linters/base"
4
+ require "e11y/registry"
5
+
6
+ module E11y
7
+ module Linters
8
+ module SLO
9
+ # Linter for explicit SLO declaration on Event classes.
10
+ #
11
+ # Ensures every registered event class has either `slo do ... end` or
12
+ # `slo false` — i.e. slo_enabled? or slo_disabled? must be true.
13
+ class ExplicitDeclarationLinter
14
+ class << self
15
+ # Validate all registered event classes have explicit SLO declaration.
16
+ #
17
+ # @raise [E11y::Linters::LinterError] when any event has neither slo_enabled? nor slo_disabled?
18
+ def validate!
19
+ errors = []
20
+
21
+ E11y::Registry.event_classes.each do |event_class|
22
+ next if event_class.slo_enabled? || event_class.slo_disabled?
23
+
24
+ name = event_class.respond_to?(:event_name) ? event_class.event_name : event_class.name
25
+ errors << "Event #{name} missing explicit SLO declaration! Add `slo do ... end` or `slo false`"
26
+ end
27
+
28
+ return if errors.empty?
29
+
30
+ raise LinterError, errors.join("\n")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/linters/base"
4
+ require "e11y/registry"
5
+
6
+ module E11y
7
+ module Linters
8
+ module SLO
9
+ # Linter for SLO-enabled events: requires slo_status_from and contributes_to.
10
+ #
11
+ # When an event has slo_enabled?, it must define:
12
+ # - slo_status_from (slo_config.slo_status_proc) — how to compute slo_status from payload
13
+ # - contributes_to (slo_config.contributes_to_value) — which custom SLO this event feeds
14
+ class SloStatusFromLinter
15
+ class << self
16
+ # Validate all SLO-enabled event classes have slo_status_from and contributes_to.
17
+ #
18
+ # @raise [E11y::Linters::LinterError] when any slo-enabled event is missing either
19
+ def validate!
20
+ errors = []
21
+
22
+ E11y::Registry.event_classes.each do |event_class|
23
+ next unless event_class.slo_enabled?
24
+
25
+ config = event_class.slo_config
26
+ name = event_class.respond_to?(:event_name) ? event_class.event_name : event_class.name
27
+
28
+ errors << "Event #{name} has slo enabled but missing slo_status_from" unless config&.slo_status_proc
29
+
30
+ errors << "Event #{name} has slo enabled but missing contributes_to" unless config&.contributes_to_value
31
+ end
32
+
33
+ return if errors.empty?
34
+
35
+ raise LinterError, errors.join("\n")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end