e11y 0.2.0 → 1.0.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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +130 -10
  3. data/CHANGELOG.md +56 -1
  4. data/CLAUDE.md +168 -0
  5. data/CONTRIBUTING.md +640 -0
  6. data/README.md +134 -702
  7. data/RELEASE.md +18 -3
  8. data/Rakefile +108 -29
  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 +29 -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} +35 -64
  30. data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
  31. data/docs/{ADR-003-slo-observability.md → architecture/ADR-003-slo-observability.md} +27 -466
  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} +209 -339
  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} +41 -83
  40. data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
  41. data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
  42. data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +23 -41
  43. data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +52 -349
  44. data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
  45. data/docs/architecture/ADR-018-memory-optimization.md +366 -0
  46. data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
  47. data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
  48. data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
  49. data/docs/prd/01-overview-vision.md +19 -14
  50. data/docs/use_cases/README.md +22 -23
  51. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
  52. data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
  53. data/docs/use_cases/UC-003-event-metrics.md +66 -0
  54. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +42 -101
  55. data/docs/use_cases/UC-005-sentry-integration.md +13 -15
  56. data/docs/use_cases/UC-006-trace-context-management.md +30 -28
  57. data/docs/use_cases/UC-007-pii-filtering.md +35 -87
  58. data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
  59. data/docs/use_cases/UC-009-multi-service-tracing.md +4 -4
  60. data/docs/use_cases/UC-010-background-job-tracking.md +5 -5
  61. data/docs/use_cases/UC-011-rate-limiting.md +95 -168
  62. data/docs/use_cases/UC-012-audit-trail.md +21 -46
  63. data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
  64. data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
  65. data/docs/use_cases/UC-015-cost-optimization.md +46 -99
  66. data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
  67. data/docs/use_cases/UC-017-local-development.md +203 -777
  68. data/docs/use_cases/UC-018-testing-events.md +3 -3
  69. data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
  70. data/docs/use_cases/UC-020-event-versioning.md +8 -9
  71. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
  72. data/docs/use_cases/UC-022-event-registry.md +15 -21
  73. data/docs/use_cases/backlog.md +119 -87
  74. data/e11y.gemspec +2 -2
  75. data/gems/e11y-devtools/README.md +136 -0
  76. data/gems/e11y-devtools/config/routes.rb +8 -0
  77. data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
  78. data/gems/e11y-devtools/exe/e11y +34 -0
  79. data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
  80. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
  81. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
  82. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
  83. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
  84. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
  85. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
  86. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
  87. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
  88. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
  89. data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +115 -0
  90. data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +54 -0
  91. data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
  92. data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
  93. data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
  94. data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
  95. data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
  96. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
  97. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
  98. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
  99. data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
  100. data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
  101. data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
  102. data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +58 -0
  103. data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
  104. data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
  105. data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
  106. data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
  107. data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
  108. data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
  109. data/lib/e11y/adapters/audit_encrypted.rb +53 -11
  110. data/lib/e11y/adapters/base.rb +33 -34
  111. data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
  112. data/lib/e11y/adapters/dev_log/query.rb +219 -0
  113. data/lib/e11y/adapters/dev_log.rb +118 -0
  114. data/lib/e11y/adapters/file.rb +3 -6
  115. data/lib/e11y/adapters/in_memory.rb +52 -5
  116. data/lib/e11y/adapters/in_memory_test.rb +29 -0
  117. data/lib/e11y/adapters/loki.rb +58 -23
  118. data/lib/e11y/adapters/null.rb +82 -0
  119. data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
  120. data/lib/e11y/adapters/otel_logs.rb +136 -23
  121. data/lib/e11y/adapters/sentry.rb +4 -7
  122. data/lib/e11y/adapters/stdout.rb +73 -7
  123. data/lib/e11y/adapters/yabeda.rb +153 -29
  124. data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
  125. data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
  126. data/lib/e11y/buffers/ring_buffer.rb +3 -16
  127. data/lib/e11y/configuration.rb +272 -0
  128. data/lib/e11y/console.rb +10 -17
  129. data/lib/e11y/current.rb +53 -1
  130. data/lib/e11y/debug/pipeline_inspector.rb +96 -0
  131. data/lib/e11y/documentation/generator.rb +48 -0
  132. data/lib/e11y/event/base.rb +176 -82
  133. data/lib/e11y/event/value_sampling_config.rb +1 -5
  134. data/lib/e11y/events/rails/database/query.rb +1 -4
  135. data/lib/e11y/events/rails/job/failed.rb +2 -0
  136. data/lib/e11y/instruments/active_job.rb +46 -12
  137. data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
  138. data/lib/e11y/instruments/sidekiq.rb +137 -31
  139. data/lib/e11y/linters/base.rb +11 -0
  140. data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
  141. data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
  142. data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
  143. data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
  144. data/lib/e11y/logger/bridge.rb +26 -7
  145. data/lib/e11y/metrics/cardinality_protection.rb +10 -15
  146. data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
  147. data/lib/e11y/metrics/registry.rb +3 -5
  148. data/lib/e11y/metrics/test_backend.rb +62 -0
  149. data/lib/e11y/metrics.rb +56 -10
  150. data/lib/e11y/middleware/adapter_resolver.rb +40 -0
  151. data/lib/e11y/middleware/audit_signing.rb +43 -6
  152. data/lib/e11y/middleware/baggage_protection.rb +75 -0
  153. data/lib/e11y/middleware/dev_log_source.rb +24 -0
  154. data/lib/e11y/middleware/event_slo.rb +23 -9
  155. data/lib/e11y/middleware/otel_span.rb +23 -0
  156. data/lib/e11y/middleware/pii_filter.rb +104 -75
  157. data/lib/e11y/middleware/rate_limiting.rb +54 -27
  158. data/lib/e11y/middleware/request.rb +70 -23
  159. data/lib/e11y/middleware/routing.rb +78 -21
  160. data/lib/e11y/middleware/sampling.rb +66 -17
  161. data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
  162. data/lib/e11y/middleware/trace_context.rb +45 -10
  163. data/lib/e11y/middleware/track_latency.rb +34 -0
  164. data/lib/e11y/middleware/validation.rb +7 -16
  165. data/lib/e11y/middleware/versioning.rb +26 -22
  166. data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
  167. data/lib/e11y/opentelemetry/span_creator.rb +142 -0
  168. data/lib/e11y/pii/patterns.rb +12 -1
  169. data/lib/e11y/pipeline/builder.rb +1 -1
  170. data/lib/e11y/presets/audit_event.rb +13 -2
  171. data/lib/e11y/railtie.rb +52 -15
  172. data/lib/e11y/registry.rb +306 -0
  173. data/lib/e11y/reliability/circuit_breaker.rb +19 -21
  174. data/lib/e11y/reliability/dlq/base.rb +71 -0
  175. data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
  176. data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
  177. data/lib/e11y/reliability/dlq/filter.rb +37 -54
  178. data/lib/e11y/reliability/retry_handler.rb +26 -29
  179. data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
  180. data/lib/e11y/sampling/error_spike_detector.rb +0 -2
  181. data/lib/e11y/sampling/load_monitor.rb +5 -9
  182. data/lib/e11y/sampling/stratified_tracker.rb +18 -0
  183. data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
  184. data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
  185. data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
  186. data/lib/e11y/slo/config_loader.rb +40 -0
  187. data/lib/e11y/slo/config_validator.rb +58 -0
  188. data/lib/e11y/slo/dashboard_generator.rb +122 -0
  189. data/lib/e11y/slo/event_driven.rb +8 -0
  190. data/lib/e11y/slo/tracker.rb +31 -4
  191. data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
  192. data/lib/e11y/testing/rspec_matchers.rb +21 -0
  193. data/lib/e11y/testing/snapshot_matcher.rb +86 -0
  194. data/lib/e11y/trace_context/sampler.rb +35 -0
  195. data/lib/e11y/tracing/faraday_middleware.rb +31 -0
  196. data/lib/e11y/tracing/net_http_patch.rb +33 -0
  197. data/lib/e11y/tracing/propagator.rb +116 -0
  198. data/lib/e11y/tracing.rb +47 -0
  199. data/lib/e11y/version.rb +1 -1
  200. data/lib/e11y/versioning/version_extractor.rb +32 -0
  201. data/lib/e11y.rb +141 -265
  202. data/lib/generators/e11y/event/event_generator.rb +22 -0
  203. data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
  204. data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
  205. data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
  206. data/lib/generators/e11y/install/install_generator.rb +34 -0
  207. data/lib/generators/e11y/install/templates/e11y.rb +239 -0
  208. data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
  209. data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
  210. data/lib/tasks/e11y_docs.rake +30 -0
  211. data/lib/tasks/e11y_events.rake +71 -0
  212. data/lib/tasks/e11y_lint.rake +91 -0
  213. data/lib/tasks/e11y_slo.rake +29 -0
  214. metadata +129 -39
  215. data/docs/ADR-010-developer-experience.md +0 -2166
  216. data/docs/API-REFERENCE-L28.md +0 -914
  217. data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
  218. data/docs/CONTRIBUTING.md +0 -312
  219. data/docs/IMPLEMENTATION_NOTES.md +0 -2804
  220. data/docs/IMPLEMENTATION_PLAN.md +0 -1971
  221. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
  222. data/docs/PLAN.md +0 -148
  223. data/docs/README.md +0 -296
  224. data/docs/design/00-memory-optimization.md +0 -593
  225. data/docs/guides/MIGRATION-L27-L28.md +0 -692
  226. data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
  227. data/docs/guides/README.md +0 -44
  228. data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
  229. data/lib/e11y/adapters/registry.rb +0 -141
  230. /data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +0 -0
@@ -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
@@ -8,7 +8,7 @@ module E11y
8
8
  #
9
9
  # Transparent wrapper around Rails.logger that:
10
10
  # 1. Delegates all calls to the original logger (preserves Rails behavior)
11
- # 2. Tracks log calls as E11y events (when logger_bridge.enabled = true)
11
+ # 2. Tracks log calls as E11y events (when logger_bridge_enabled = true)
12
12
  #
13
13
  # **Why SimpleDelegator instead of full replacement:**
14
14
  # - ✅ Simpler: No need to reimplement entire Logger API
@@ -17,12 +17,12 @@ module E11y
17
17
  # - ✅ Rails Way: Extends functionality without replacing core components
18
18
  #
19
19
  # @example Basic usage
20
- # # Automatically enabled by E11y::Railtie if config.logger_bridge.enabled = true
20
+ # # Automatically enabled by E11y::Railtie if config.logger_bridge_enabled = true
21
21
  # Rails.logger = E11y::Logger::Bridge.new(Rails.logger)
22
22
  #
23
23
  # @example Manual setup
24
24
  # E11y.configure do |config|
25
- # config.logger_bridge.enabled = true # Wrap Rails.logger and send logs to E11y
25
+ # config.logger_bridge_enabled = true # Wrap Rails.logger and send logs to E11y
26
26
  # end
27
27
  #
28
28
  # @see ADR-008 §7 (Rails.logger Migration)
@@ -34,7 +34,7 @@ module E11y
34
34
  #
35
35
  # @return [void]
36
36
  def self.setup!
37
- return unless E11y.config.logger_bridge&.enabled
37
+ return unless E11y.config.logger_bridge_enabled
38
38
  return unless defined?(::Rails)
39
39
 
40
40
  # Wrap Rails.logger (preserves original behavior)
@@ -53,6 +53,8 @@ module E11y
53
53
  ::Logger::FATAL => :fatal,
54
54
  ::Logger::UNKNOWN => :warn
55
55
  }
56
+ @track_severities_set = build_track_severities_set(E11y.config.logger_bridge_track_severities)
57
+ @ignore_patterns = build_compiled_patterns(E11y.config.logger_bridge_ignore_patterns)
56
58
  end
57
59
 
58
60
  # Intercept logger methods to track to E11y
@@ -124,18 +126,22 @@ module E11y
124
126
  # @param message [String, nil] Log message
125
127
  # @yield Block that returns log message
126
128
  # @return [void]
127
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
128
129
  # Logger tracking requires message extraction, validation, event class lookup, and error handling
129
130
  def track_to_e11y(severity, message = nil)
130
131
  # Extract message
131
132
  msg = message || (block_given? ? yield : nil)
132
133
  return if msg.nil? || (msg.respond_to?(:empty?) && msg.empty?)
133
134
 
135
+ msg_str = msg.to_s
136
+
137
+ return if @track_severities_set && !@track_severities_set.include?(severity)
138
+ return if @ignore_patterns.any? { |re| re.match?(msg_str) }
139
+
134
140
  # Track to E11y using severity-specific class
135
141
  require "e11y/events/rails/log"
136
142
  event_class = event_class_for_severity(severity)
137
143
  event_class.track(
138
- message: msg.to_s,
144
+ message: msg_str,
139
145
  caller_location: extract_caller_location
140
146
  )
141
147
  rescue StandardError => e
@@ -143,7 +149,6 @@ module E11y
143
149
  # In development/test, you might want to log this
144
150
  warn "E11y logger tracking failed: #{e.message}" if defined?(::Rails) && ::Rails.env.development?
145
151
  end
146
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
147
152
 
148
153
  # Get event class for severity
149
154
  # @param severity [Symbol] E11y severity
@@ -162,6 +167,20 @@ module E11y
162
167
  end
163
168
  # rubocop:enable Lint/DuplicateBranch
164
169
 
170
+ def build_track_severities_set(severities)
171
+ return nil if severities.nil? || (severities.respond_to?(:empty?) && severities.empty?)
172
+
173
+ Set.new(Array(severities).map(&:to_sym))
174
+ end
175
+
176
+ def build_compiled_patterns(patterns)
177
+ return [] if patterns.nil? || !patterns.respond_to?(:any?) || patterns.none?
178
+
179
+ Array(patterns).map do |p|
180
+ p.is_a?(Regexp) ? p : Regexp.new(Regexp.escape(p.to_s))
181
+ end.freeze
182
+ end
183
+
165
184
  # Extract caller location (first caller outside E11y)
166
185
  # @return [String, nil] Caller location string
167
186
  def extract_caller_location
@@ -7,11 +7,10 @@ module E11y
7
7
  module Metrics
8
8
  # Cardinality protection for metrics labels.
9
9
  #
10
- # Implements 4-layer defense system to prevent cardinality explosions:
10
+ # Implements 3-layer defense system to prevent cardinality explosions:
11
11
  # 1. Universal Denylist - Block high-cardinality fields (user_id, order_id, etc.)
12
12
  # 2. Per-Metric Limits - Track unique values per metric, drop if exceeded
13
- # 3. Dynamic Monitoring - Alert when approaching limits
14
- # 4. Dynamic Actions - Auto-relabeling, alerting, or dropping on overflow
13
+ # 3. Dynamic Actions - Drop, alert, or relabel on overflow
15
14
  #
16
15
  # Now supports optional relabeling to reduce cardinality while preserving signal.
17
16
  #
@@ -37,7 +36,7 @@ module E11y
37
36
  # @see ADR-002 §4 (Cardinality Protection)
38
37
  # @see UC-013 (High Cardinality Protection)
39
38
  # rubocop:disable Metrics/ClassLength
40
- # Cardinality protection is a cohesive 4-layer defense system against metric explosions
39
+ # Cardinality protection is a cohesive 3-layer defense system against metric explosions
41
40
  class CardinalityProtection
42
41
  # Universal denylist - high-cardinality fields that should NEVER be labels
43
42
  UNIVERSAL_DENYLIST = %i[
@@ -64,7 +63,7 @@ module E11y
64
63
  # Default per-metric cardinality limit
65
64
  DEFAULT_CARDINALITY_LIMIT = 1000
66
65
 
67
- # Overflow strategies (Layer 4: Dynamic Actions)
66
+ # Overflow strategies (Layer 3: Dynamic Actions)
68
67
  OVERFLOW_STRATEGIES = %i[drop alert relabel].freeze
69
68
 
70
69
  # Default overflow strategy
@@ -85,7 +84,6 @@ module E11y
85
84
  # @option config [Float] :alert_threshold (0.8) Alert when cardinality reaches this ratio
86
85
  # @option config [Proc] :alert_callback Optional callback when alert triggered
87
86
  # @option config [Boolean] :auto_relabel (false) Auto-relabel to [OTHER] on overflow
88
- # rubocop:disable Metrics/AbcSize
89
87
  # Cardinality protection initialization requires extracting multiple config options and setting up components
90
88
  def initialize(config = {})
91
89
  @cardinality_limit = config.fetch(:cardinality_limit, DEFAULT_CARDINALITY_LIMIT)
@@ -109,7 +107,6 @@ module E11y
109
107
  @overflow_counts = Hash.new(0)
110
108
  @overflow_mutex = Mutex.new
111
109
  end
112
- # rubocop:enable Metrics/AbcSize
113
110
 
114
111
  # Define relabeling rule for a label
115
112
  #
@@ -222,7 +219,6 @@ module E11y
222
219
 
223
220
  # Check if approaching alert threshold (Layer 3: Monitoring)
224
221
  # @param metric_name [String] Metric name
225
- # rubocop:disable Metrics/MethodLength
226
222
  # Alert threshold checking requires calculating ratio, checking conditions, and sending detailed alerts
227
223
  def check_alert_threshold(metric_name)
228
224
  return unless @alert_threshold
@@ -252,7 +248,6 @@ module E11y
252
248
  # Track metric
253
249
  track_cardinality_metric(metric_name, :threshold_exceeded, current_cardinality)
254
250
  end
255
- # rubocop:enable Metrics/MethodLength
256
251
 
257
252
  # Handle overflow when cardinality limit exceeded (Layer 4: Dynamic Actions)
258
253
  # @param metric_name [String] Metric name
@@ -311,9 +306,11 @@ module E11y
311
306
  severity: :error
312
307
  )
313
308
 
314
- # Also log warning
315
- warn "E11y Metrics: Cardinality limit exceeded for #{metric_name}:#{key} " \
316
- "(limit: #{@cardinality_limit}, current: #{current_cardinality})"
309
+ # Also log warning (via E11y.logger so it respects Rails.logger in test env)
310
+ E11y.logger&.warn(
311
+ "E11y Metrics: Cardinality limit exceeded for #{metric_name}:#{key} " \
312
+ "(limit: #{@cardinality_limit}, current: #{current_cardinality})"
313
+ )
317
314
  end
318
315
 
319
316
  # Handle relabel strategy - relabel to [OTHER]
@@ -385,7 +382,6 @@ module E11y
385
382
  # @param metric_name [String] Metric name
386
383
  # @param action [Symbol] Action type (:threshold_exceeded, :drop, :alert, :relabel)
387
384
  # @param value [Integer] Metric value
388
- # rubocop:disable Metrics/MethodLength
389
385
  # Cardinality tracking requires incrementing overflow counters and updating gauge metrics
390
386
  def track_cardinality_metric(metric_name, action, value)
391
387
  return unless defined?(E11y::Metrics)
@@ -408,9 +404,8 @@ module E11y
408
404
  )
409
405
  rescue StandardError => e
410
406
  # Don't fail on metrics tracking errors
411
- warn "E11y: Failed to track cardinality metric: #{e.message}"
407
+ E11y.logger&.warn("E11y: Failed to track cardinality metric: #{e.message}")
412
408
  end
413
- # rubocop:enable Metrics/MethodLength
414
409
  end
415
410
  # rubocop:enable Metrics/ClassLength
416
411
  end
@@ -33,13 +33,15 @@ module E11y
33
33
  # Records unique label values per metric+label combination.
34
34
  # Thread-safe operation.
35
35
  #
36
- # @param metric_name [String] Metric name
36
+ # @param metric_name [String, Symbol] Metric name
37
37
  # @param label_key [Symbol, String] Label key
38
38
  # @param label_value [Object] Label value to track
39
39
  # @return [Boolean] true if within limit, false if limit exceeded
40
40
  def track(metric_name, label_key, label_value)
41
41
  @mutex.synchronize do
42
- value_set = @tracker[metric_name][label_key]
42
+ # Normalize metric_name to string for consistent key access
43
+ metric_key = metric_name.to_s
44
+ value_set = @tracker[metric_key][label_key]
43
45
 
44
46
  # Allow if already tracked (existing value)
45
47
  return true if value_set.include?(label_value)
@@ -66,7 +68,9 @@ module E11y
66
68
  # @return [void]
67
69
  def force_track(metric_name, label_key, label_value)
68
70
  @mutex.synchronize do
69
- value_set = @tracker[metric_name][label_key]
71
+ # Normalize metric_name to string for consistent key access
72
+ metric_key = metric_name.to_s
73
+ value_set = @tracker[metric_key][label_key]
70
74
  value_set.add(label_value) unless value_set.include?(label_value)
71
75
  end
72
76
  end
@@ -78,7 +82,9 @@ module E11y
78
82
  # @return [Boolean] true if at or above limit
79
83
  def exceeded?(metric_name, label_key)
80
84
  @mutex.synchronize do
81
- @tracker.dig(metric_name, label_key)&.size.to_i >= @limit
85
+ # Normalize metric_name to string for consistent key access
86
+ metric_key = metric_name.to_s
87
+ @tracker.dig(metric_key, label_key)&.size.to_i >= @limit
82
88
  end
83
89
  end
84
90
 
@@ -89,7 +95,9 @@ module E11y
89
95
  # @return [Integer] Number of unique values tracked
90
96
  def cardinality(metric_name, label_key)
91
97
  @mutex.synchronize do
92
- @tracker.dig(metric_name, label_key)&.size || 0
98
+ # Normalize metric_name to string for consistent key access
99
+ metric_key = metric_name.to_s
100
+ @tracker.dig(metric_key, label_key)&.size || 0
93
101
  end
94
102
  end
95
103
 
@@ -99,7 +107,9 @@ module E11y
99
107
  # @return [Hash{Symbol => Integer}] Label key => cardinality
100
108
  def cardinalities(metric_name)
101
109
  @mutex.synchronize do
102
- metric_data = @tracker[metric_name]
110
+ # Normalize metric_name to string for consistent key access
111
+ metric_key = metric_name.to_s
112
+ metric_data = @tracker[metric_key]
103
113
  metric_data.transform_values(&:size)
104
114
  end
105
115
  end
@@ -6,7 +6,7 @@ module E11y
6
6
  module Metrics
7
7
  # Registry for metric configurations.
8
8
  #
9
- # Stores metric definitions and provides pattern-based matching.
9
+ # Stores metric definitions and provides event-name matching.
10
10
  # This is a singleton class - use Registry.instance to access it.
11
11
  # All metrics (global, event-level, preset) are registered here for validation.
12
12
  #
@@ -144,7 +144,6 @@ module E11y
144
144
  # Validate metric configuration
145
145
  # @param config [Hash] Metric configuration
146
146
  # @raise [ArgumentError] if configuration is invalid
147
- # rubocop:disable Metrics/AbcSize
148
147
  def validate_config!(config)
149
148
  raise ArgumentError, "Metric type is required" unless config[:type]
150
149
  raise ArgumentError, "Invalid metric type: #{config[:type]}" unless %i[counter histogram
@@ -156,14 +155,13 @@ module E11y
156
155
 
157
156
  raise ArgumentError, "Value extractor is required for #{config[:type]} metrics"
158
157
  end
159
- # rubocop:enable Metrics/AbcSize
160
158
 
161
159
  # Validate that new metric doesn't conflict with existing one
162
160
  # @param existing [Hash] Existing metric configuration
163
161
  # @param new_config [Hash] New metric configuration
164
162
  # @raise [TypeConflictError] if types don't match
165
163
  # @raise [LabelConflictError] if labels don't match
166
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
164
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
167
165
  # Conflict validation requires checking type and labels with detailed error messages
168
166
  def validate_no_conflicts!(existing, new_config)
169
167
  # Check 1: Type must match
@@ -215,7 +213,7 @@ module E11y
215
213
  Using existing buckets.
216
214
  WARNING
217
215
  end
218
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
216
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
219
217
 
220
218
  # Compile glob pattern to regex
221
219
  # @param pattern [String] Glob pattern (e.g., "order.*", "user.*.created")
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Metrics
5
+ # In-memory metrics backend for tests.
6
+ #
7
+ # Records all metric calls so test assertions can verify what was tracked
8
+ # without using mocks on E11y::Metrics directly.
9
+ #
10
+ # @example
11
+ # backend = E11y::Metrics::TestBackend.new
12
+ # E11y::Metrics.instance_variable_set(:@backend, backend)
13
+ #
14
+ # MyService.call
15
+ #
16
+ # expect(backend.increment_count(:orders_total)).to eq(1)
17
+ # expect(backend.increments).to include(hash_including(name: :orders_total))
18
+ class TestBackend
19
+ attr_reader :increments, :histograms, :gauges
20
+
21
+ def initialize
22
+ reset!
23
+ end
24
+
25
+ # @param name [Symbol] Metric name
26
+ # @param labels [Hash] Metric labels
27
+ # @param value [Integer] Increment amount
28
+ def increment(name, labels = {}, value: 1)
29
+ @increments << { name: name, labels: labels, value: value }
30
+ end
31
+
32
+ # @param name [Symbol] Metric name
33
+ # @param value [Numeric] Observed value
34
+ # @param labels [Hash] Metric labels
35
+ def histogram(name, value, labels = {}, buckets: nil) # rubocop:todo Lint/UnusedMethodArgument
36
+ @histograms << { name: name, value: value, labels: labels }
37
+ end
38
+
39
+ # @param name [Symbol] Metric name
40
+ # @param value [Numeric] Gauge value
41
+ # @param labels [Hash] Metric labels
42
+ def gauge(name, value, labels = {})
43
+ @gauges << { name: name, value: value, labels: labels }
44
+ end
45
+
46
+ # Reset all recorded metrics.
47
+ def reset!
48
+ @increments = []
49
+ @histograms = []
50
+ @gauges = []
51
+ end
52
+
53
+ # Count how many times a counter was incremented (any labels).
54
+ #
55
+ # @param name [Symbol] Metric name
56
+ # @return [Integer]
57
+ def increment_count(name)
58
+ @increments.count { |r| r[:name] == name }
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/e11y/metrics.rb CHANGED
@@ -21,18 +21,41 @@ module E11y
21
21
  # @see ADR-002 §3 (Metrics Integration)
22
22
  # @see ADR-016 §3 (Self-Monitoring Metrics)
23
23
  module Metrics
24
+ # No-op metrics backend used when no real backend (e.g. Yabeda) is configured.
25
+ # Accepts all metric calls and silently discards them so callers never
26
+ # need to guard against a nil backend.
27
+ class NullBackend
28
+ def increment(_name, _labels = {}, value: 1); end
29
+ def histogram(_name, _value, _labels = {}, buckets: nil); end
30
+ def gauge(_name, _value, _labels = {}); end
31
+ end
32
+
24
33
  class << self
25
34
  # Track a counter metric (monotonically increasing value).
26
35
  #
27
- # @param name [Symbol] Metric name (e.g., :http_requests_total)
36
+ # Accepts dotted names (e.g., "e11y.ephemeral_buffer.flushed") and normalizes to
37
+ # underscores. DLQ metrics get _total suffix. Labels[:events] is used as value if present.
38
+ # Safe: no-op when backend unavailable, rescues errors.
39
+ #
40
+ # @param name [Symbol, String] Metric name (e.g., :http_requests_total or "e11y.ephemeral_buffer.flushed")
28
41
  # @param labels [Hash] Metric labels (e.g., { method: 'GET', status: 200 })
29
- # @param value [Integer] Increment value (default: 1)
42
+ # @param value [Integer] Increment value (default: 1, overridden by labels[:events] if present)
30
43
  # @return [void]
31
44
  #
32
45
  # @example
33
- # E11y::Metrics.increment(:e11y_events_tracked, { event_type: 'order.created' })
34
- def increment(name, labels = {}, value: 1)
35
- backend&.increment(name, labels, value: value)
46
+ # E11y::Metrics.increment(:e11y_events_tracked, event_type: 'order.created')
47
+ # E11y::Metrics.increment("e11y.ephemeral_buffer.flushed_on_error", value: 5)
48
+ def increment(name, labels = {}, value: 1, **labels_kw)
49
+ return unless backend
50
+
51
+ labels = labels.merge(labels_kw) unless labels_kw.empty?
52
+ value = labels.delete(:events) if labels.key?(:events)
53
+ value ||= 1
54
+
55
+ normalized = normalized_metric_name(name)
56
+ backend.increment(normalized, labels, value: value)
57
+ rescue StandardError => e
58
+ E11y.logger&.debug("E11y metrics: #{e.message}")
36
59
  end
37
60
 
38
61
  # Track a histogram metric (distribution of values).
@@ -44,9 +67,15 @@ module E11y
44
67
  # @return [void]
45
68
  #
46
69
  # @example
47
- # E11y::Metrics.histogram(:e11y_track_duration_seconds, 0.0005, { event_type: 'order.created' })
48
- def histogram(name, value, labels = {}, buckets: nil)
49
- backend&.histogram(name, value, labels, buckets: buckets)
70
+ # E11y::Metrics.histogram(:e11y_track_duration_seconds, 0.0005, event_type: 'order.created')
71
+ def histogram(name, value, labels = {}, buckets: nil, **labels_kw)
72
+ return unless backend
73
+
74
+ labels = labels.merge(labels_kw) unless labels_kw.empty?
75
+ normalized = normalized_metric_name(name)
76
+ backend.histogram(normalized, value, labels, buckets: buckets)
77
+ rescue StandardError => e
78
+ E11y.logger&.debug("E11y metrics: #{e.message}")
50
79
  end
51
80
 
52
81
  # Track a gauge metric (current value that can go up or down).
@@ -81,10 +110,27 @@ module E11y
81
110
  # @api private
82
111
  def reset_backend!
83
112
  remove_instance_variable(:@backend) if defined?(@backend)
113
+ @name_cache = nil if defined?(@name_cache)
84
114
  end
85
115
 
86
116
  private
87
117
 
118
+ # Normalize metric name: dots to underscores, DLQ metrics get _total suffix.
119
+ # Cached to avoid repeated string allocations for hot-path metrics.
120
+ #
121
+ # @param name [Symbol, String] Raw metric name
122
+ # @return [Symbol] Normalized name for Prometheus (e.g., e11y_ephemeral_buffer_flushed_on_error)
123
+ def normalized_metric_name(name)
124
+ @name_cache ||= {}
125
+ @name_cache[name] ||= compute_normalized_name(name)
126
+ end
127
+
128
+ def compute_normalized_name(name)
129
+ s = name.to_s.tr(".", "_")
130
+ s = "#{s}_total" if s.include?("e11y_dlq_") && !s.end_with?("_total")
131
+ s.to_sym
132
+ end
133
+
88
134
  # Detect the metrics backend from configured adapters.
89
135
  #
90
136
  # @return [Object, nil] Metrics backend or nil
@@ -99,8 +145,8 @@ module E11y
99
145
  # rubocop:enable Style/ClassEqualityComparison
100
146
  return yabeda_adapter if yabeda_adapter
101
147
 
102
- # No backend configured noop
103
- nil
148
+ # No Yabeda adapter configured fall back to NullBackend
149
+ NullBackend.new
104
150
  end
105
151
  end
106
152
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Middleware
5
+ # Resolves target adapter names for an event (shared by PIIFilter and Routing).
6
+ #
7
+ # @api private
8
+ module AdapterResolver
9
+ # Resolve target adapters for event_data (explicit or routing rules).
10
+ #
11
+ # @param event_data [Hash] Event data with :adapters, :audit_event, :retention_until, etc.
12
+ # @return [Array<Symbol>] Target adapter names
13
+ def self.resolve(event_data)
14
+ if event_data[:adapters]&.any?
15
+ Array(event_data[:adapters]).map(&:to_sym)
16
+ else
17
+ apply_routing_rules(event_data)
18
+ end
19
+ end
20
+
21
+ def self.apply_routing_rules(event_data)
22
+ matched_adapters = []
23
+ rules = E11y.configuration.routing_rules || []
24
+
25
+ rules.each do |rule|
26
+ result = rule.call(event_data)
27
+ matched_adapters.concat(Array(result)) if result
28
+ rescue StandardError => e
29
+ warn "[E11y] Routing rule error: #{e.message}"
30
+ end
31
+
32
+ if matched_adapters.any?
33
+ matched_adapters.uniq
34
+ else
35
+ E11y.configuration.fallback_adapters || [:stdout]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end