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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module E11y
4
4
  module Buffers
5
- # Request-scoped buffer using thread-local storage for debug event buffering.
5
+ # Ephemeral buffer (request/job-scoped) using thread-local storage for debug event buffering.
6
6
  #
7
7
  # This buffer stores debug events in thread-local storage during request processing.
8
8
  # Events are flushed only when an error occurs, keeping logs clean during successful requests.
@@ -12,21 +12,21 @@ module E11y
12
12
  #
13
13
  # @example Basic usage
14
14
  # # In Rails middleware
15
- # RequestScopedBuffer.initialize!
15
+ # EphemeralBuffer.initialize!
16
16
  #
17
17
  # # Track debug events (buffered)
18
- # RequestScopedBuffer.add_event({ event_name: "debug", severity: :debug })
18
+ # EphemeralBuffer.add_event({ event_name: "debug", severity: :debug })
19
19
  #
20
20
  # # On error - flush all buffered events
21
- # RequestScopedBuffer.flush_on_error
21
+ # EphemeralBuffer.flush_on_error
22
22
  #
23
23
  # # On success - discard buffered events
24
- # RequestScopedBuffer.discard
24
+ # EphemeralBuffer.discard
25
25
  #
26
26
  # @see UC-001 Request-Scoped Debug Buffering
27
- class RequestScopedBuffer
27
+ class EphemeralBuffer
28
28
  # Thread-local storage keys
29
- THREAD_KEY_BUFFER = :e11y_request_buffer
29
+ THREAD_KEY_BUFFER = :e11y_ephemeral_buffer
30
30
  THREAD_KEY_REQUEST_ID = :e11y_request_id
31
31
  THREAD_KEY_ERROR_OCCURRED = :e11y_error_occurred
32
32
  THREAD_KEY_BUFFER_LIMIT = :e11y_buffer_limit
@@ -42,7 +42,7 @@ module E11y
42
42
  # @return [void]
43
43
  #
44
44
  # @example
45
- # RequestScopedBuffer.initialize!(request_id: "req-123", buffer_limit: 200)
45
+ # EphemeralBuffer.initialize!(request_id: "req-123", buffer_limit: 200)
46
46
  def initialize!(request_id: nil, buffer_limit: DEFAULT_BUFFER_LIMIT)
47
47
  Thread.current[THREAD_KEY_BUFFER] = []
48
48
  Thread.current[THREAD_KEY_REQUEST_ID] = request_id || generate_request_id
@@ -60,42 +60,19 @@ module E11y
60
60
  #
61
61
  # @example
62
62
  # # Debug event - buffered
63
- # RequestScopedBuffer.add_event({ event_name: "test", severity: :debug })
63
+ # EphemeralBuffer.add_event({ event_name: "test", severity: :debug })
64
64
  # # => true
65
65
  #
66
66
  # # Error event - not buffered, triggers flush
67
- # RequestScopedBuffer.add_event({ event_name: "error", severity: :error })
67
+ # EphemeralBuffer.add_event({ event_name: "error", severity: :error })
68
68
  # # => false (and flushes buffer)
69
- # rubocop:disable Metrics/MethodLength, Naming/PredicateMethod
70
69
  def add_event(event_data)
71
- return false unless active? # Not in request scope
70
+ return false unless active?
71
+ return handle_error_event(event_data) if error_severity?(event_data[:severity])
72
+ return false unless event_data[:severity] == :debug
72
73
 
73
- severity = event_data[:severity]
74
-
75
- # Trigger flush on error severity
76
- if error_severity?(severity)
77
- Thread.current[THREAD_KEY_ERROR_OCCURRED] = true
78
- flush_on_error
79
- return false # Error events not buffered
80
- end
81
-
82
- # Only buffer debug events
83
- return false unless severity == :debug
84
-
85
- current_buffer = buffer
86
- return false if current_buffer.nil?
87
-
88
- # Check buffer limit
89
- if current_buffer.size >= buffer_limit
90
- increment_metric("e11y.request_buffer.overflow")
91
- return false # Buffer full, drop event
92
- end
93
-
94
- current_buffer << event_data
95
- increment_metric("e11y.request_buffer.events_buffered")
96
- true
74
+ append_to_buffer(event_data)
97
75
  end
98
- # rubocop:enable Metrics/MethodLength, Naming/PredicateMethod
99
76
 
100
77
  # Flush buffered events on error
101
78
  #
@@ -108,7 +85,7 @@ module E11y
108
85
  # @example
109
86
  # # In rescue block
110
87
  # rescue StandardError => e
111
- # RequestScopedBuffer.flush_on_error
88
+ # EphemeralBuffer.flush_on_error
112
89
  # raise
113
90
  # end
114
91
  def flush_on_error(target: nil)
@@ -117,15 +94,18 @@ module E11y
117
94
 
118
95
  flushed_count = current_buffer.size
119
96
 
97
+ # Resolve flush targets once per flush (avoids N config lookups when flushing N events)
98
+ flush_targets = resolve_flush_targets
99
+
120
100
  # Flush events to main buffer/adapters
121
101
  current_buffer.each do |event_data|
122
102
  # TODO: Send to E11y::Collector.collect(event_data) when available
123
103
  # For now, placeholder
124
- flush_event(event_data, target: target)
104
+ flush_event(event_data, target: target, flush_targets: flush_targets)
125
105
  end
126
106
 
127
107
  current_buffer.clear
128
- increment_metric("e11y.request_buffer.flushed_on_error", tags: { events: flushed_count })
108
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "flushed_on_error", value: flushed_count)
129
109
  flushed_count
130
110
  end
131
111
 
@@ -135,8 +115,8 @@ module E11y
135
115
  #
136
116
  # @example
137
117
  # # In middleware ensure block (success path)
138
- # unless RequestScopedBuffer.error_occurred?
139
- # RequestScopedBuffer.discard
118
+ # unless EphemeralBuffer.error_occurred?
119
+ # EphemeralBuffer.discard
140
120
  # end
141
121
  def discard
142
122
  current_buffer = buffer
@@ -144,7 +124,7 @@ module E11y
144
124
 
145
125
  discarded_count = current_buffer.size
146
126
  current_buffer.clear
147
- increment_metric("e11y.request_buffer.discarded", tags: { events: discarded_count })
127
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "discarded", value: discarded_count)
148
128
  discarded_count
149
129
  end
150
130
 
@@ -195,6 +175,28 @@ module E11y
195
175
 
196
176
  private
197
177
 
178
+ def handle_error_event(_event_data) # rubocop:disable Naming/PredicateMethod
179
+ Thread.current[THREAD_KEY_ERROR_OCCURRED] = true
180
+ flush_on_error
181
+ false
182
+ end
183
+
184
+ def append_to_buffer(event_data)
185
+ current_buffer = buffer
186
+ return false if current_buffer.nil?
187
+ return record_buffer_overflow if current_buffer.size >= buffer_limit
188
+
189
+ event_to_store = event_data.merge(request_id: request_id)
190
+ current_buffer << event_to_store
191
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "events_buffered")
192
+ true
193
+ end
194
+
195
+ def record_buffer_overflow # rubocop:disable Naming/PredicateMethod
196
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "overflow")
197
+ false
198
+ end
199
+
198
200
  # Get buffer limit (with fallback)
199
201
  #
200
202
  # @return [Integer] Buffer limit
@@ -218,27 +220,39 @@ module E11y
218
220
  SecureRandom.uuid
219
221
  end
220
222
 
221
- # Flush single event to adapters
223
+ # Resolve flush targets: adapter instances (when debug_adapters set) or nil (use pipeline).
224
+ # Cached per flush to avoid repeated config lookups when flushing N events.
222
225
  #
223
- # @param event_data [Hash] Event to flush
224
- # @param target [Symbol, nil] Optional target adapter (not yet implemented)
225
- # @return [void]
226
- def flush_event(_event_data, target: nil) # rubocop:disable Lint/UnusedMethodArgument
227
- # Placeholder for E11y::Collector integration
228
- # Will be implemented when Collector/Adapter classes are available
226
+ # @return [Array<Object>, nil] Adapter instances to write to, or nil to use pipeline
227
+ def resolve_flush_targets
228
+ da = E11y.config.ephemeral_buffer_debug_adapters
229
+ return nil unless da&.any?
229
230
 
230
- # For now, just increment metric
231
- increment_metric("e11y.request_buffer.event_flushed")
231
+ da.filter_map { |name| E11y.configuration.adapters[name] }
232
232
  end
233
233
 
234
- # Increment metric (placeholder)
234
+ # Flush single event to adapters via pipeline or debug_adapters
235
235
  #
236
- # @param metric_name [String] Metric name
237
- # @param tags [Hash] Optional tags
236
+ # When config.ephemeral_buffer_debug_adapters is set, sends directly to those
237
+ # adapters. Otherwise uses the full pipeline (fallback_adapters).
238
+ #
239
+ # @param event_data [Hash] Event to flush
240
+ # @param target [Symbol, nil] Optional target adapter (not yet implemented)
241
+ # @param flush_targets [Array<Object>, nil] Pre-resolved adapter instances (from flush_on_error)
238
242
  # @return [void]
239
- def increment_metric(metric_name, tags: {})
240
- # Placeholder for Yabeda integration
241
- # Will be implemented in Phase 1 L2.4 (Metrics)
243
+ def flush_event(event_data, target: nil, flush_targets: nil) # rubocop:disable Lint/UnusedMethodArgument
244
+ return unless event_data
245
+
246
+ event_to_send = event_data.merge(from_ephemeral_buffer_flush: true)
247
+ targets = flush_targets || resolve_flush_targets
248
+
249
+ if targets
250
+ targets.each { |adapter| adapter&.write(event_to_send) }
251
+ else
252
+ E11y.config.built_pipeline.call(event_to_send)
253
+ end
254
+
255
+ E11y::Metrics.increment(:e11y_ephemeral_buffer_total, event: "event_flushed")
242
256
  end
243
257
  end
244
258
  end
@@ -165,7 +165,7 @@ module E11y
165
165
  # @return [Array<Hash>] All buffered events
166
166
  #
167
167
  # @example
168
- # all_events = buffer.flush_all
168
+ # events = buffer.flush_all
169
169
  # # => [event1, event2, ...]
170
170
  def flush_all
171
171
  pop(@size.value)
@@ -208,7 +208,6 @@ module E11y
208
208
  #
209
209
  # @param event [Hash] Event that caused overflow
210
210
  # @return [Boolean] true if event was eventually added, false if dropped
211
- # rubocop:disable Metrics/MethodLength
212
211
  def handle_overflow(event)
213
212
  case @overflow_strategy
214
213
  when :drop_oldest
@@ -217,21 +216,20 @@ module E11y
217
216
  push(event) # Retry push (recursive, but will succeed)
218
217
  when :drop_newest
219
218
  # Drop new event, keep buffer unchanged
220
- increment_metric("e11y.buffer.overflow.drop_newest")
219
+ E11y::Metrics.increment(:e11y_buffer_overflow_total, event: "drop_newest")
221
220
  false
222
221
  when :block
223
222
  # Wait for space, with timeout
224
223
  wait_for_space
225
224
  if full?
226
225
  # Timeout reached, drop event
227
- increment_metric("e11y.buffer.overflow.block_timeout")
226
+ E11y::Metrics.increment(:e11y_buffer_overflow_total, event: "block_timeout")
228
227
  false
229
228
  else
230
229
  push(event) # Retry after space freed
231
230
  end
232
231
  end
233
232
  end
234
- # rubocop:enable Metrics/MethodLength
235
233
 
236
234
  # Wait for buffer space (with timeout)
237
235
  #
@@ -251,17 +249,6 @@ module E11y
251
249
  sleep 0.001 # 1ms
252
250
  end
253
251
  end
254
-
255
- # Increment metric (placeholder for Phase 3: Metrics)
256
- #
257
- # TODO Phase 3: Replace with actual Yabeda metrics
258
- #
259
- # @param metric_name [String] Metric to increment
260
- # @return [void]
261
- def increment_metric(metric_name)
262
- # Placeholder - will be implemented in Phase 3
263
- # Yabeda.e11y.buffer_overflow.increment(strategy: @overflow_strategy)
264
- end
265
252
  end
266
253
  end
267
254
  end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ # Configuration class for E11y.
5
+ #
6
+ # Adapters are referenced by name (e.g., :logs, :errors_tracker).
7
+ # The actual implementation (Loki, Sentry, etc.) is configured separately.
8
+ #
9
+ # @example Configure adapters
10
+ # E11y.configure do |config|
11
+ # config.adapters[:logs] = E11y::Adapters::Loki.new(url: "...")
12
+ # config.adapters[:errors_tracker] = E11y::Adapters::Sentry.new(dsn: "...")
13
+ # end
14
+ #
15
+ # @example Configure severity => adapter mapping
16
+ # E11y.configure do |config|
17
+ # config.adapter_mapping[:error] = [:logs, :errors_tracker]
18
+ # config.adapter_mapping[:info] = [:logs]
19
+ # end
20
+ #
21
+ # @example Configure middleware pipeline
22
+ # E11y.configure do |config|
23
+ # config.pipeline.use E11y::Middleware::Sampling, default_sample_rate: 0.1
24
+ # end
25
+ class Configuration
26
+ attr_accessor :adapters, :log_level, :logger, :enabled, :environment, :service_name, :default_retention_period,
27
+ :routing_rules, :fallback_adapters, :enable_http_tracing,
28
+ :rails_instrumentation_enabled, :rails_instrumentation_custom_mappings, :rails_instrumentation_ignore_events,
29
+ :logger_bridge_enabled, :logger_bridge_track_severities, :logger_bridge_ignore_patterns,
30
+ :sidekiq_enabled, :active_job_enabled,
31
+ :ephemeral_buffer_enabled, :ephemeral_buffer_flush_on_error, :ephemeral_buffer_flush_on_statuses,
32
+ :ephemeral_buffer_debug_adapters, :ephemeral_buffer_job_buffer_limit,
33
+ :error_handling_fail_on_error,
34
+ :rate_limiting_enabled, :rate_limiting_global_limit, :rate_limiting_global_window,
35
+ :rate_limiting_per_event_limit, :rate_limiting_per_event_limits,
36
+ :slo_tracking_enabled, :slo_tracking_http_ignore_statuses, :slo_tracking_latency_percentiles,
37
+ :slo_tracking_controller_configs, :slo_tracking_job_configs,
38
+ :security_baggage_protection_enabled, :security_baggage_protection_allowed_keys, :security_baggage_protection_block_mode,
39
+ :tracing_source, :tracing_default_sample_rate, :tracing_respect_parent_sampling,
40
+ :tracing_per_event_sample_rates, :tracing_always_sample_if,
41
+ :opentelemetry_span_creation_patterns,
42
+ :cardinality_protection_max_cardinality_limit, :cardinality_protection_denylist, :cardinality_protection_overflow_strategy,
43
+ :dlq_storage, :dlq_filter
44
+ attr_reader :adapter_mapping, :pipeline
45
+
46
+ def initialize
47
+ initialize_basic_config
48
+ initialize_routing_config
49
+ initialize_feature_configs
50
+ configure_default_pipeline
51
+ end
52
+
53
+ # Get adapters for given severity
54
+ #
55
+ # @param severity [Symbol] Severity level
56
+ # @return [Array<Symbol>] Adapter names (e.g., [:logs, :errors_tracker])
57
+ def adapters_for_severity(severity)
58
+ @adapter_mapping[severity] || @adapter_mapping[:default] || []
59
+ end
60
+
61
+ # Get built pipeline (cached after first call)
62
+ #
63
+ # @return [#call] Built middleware pipeline
64
+ def built_pipeline
65
+ @built_pipeline ||= @pipeline.build(->(_event_data) {})
66
+ end
67
+
68
+ # Add per-event rate limit rule.
69
+ #
70
+ # @param pattern [String] Event name or glob pattern (e.g. "payment.*")
71
+ # @param limit [Integer] Max events per window for this pattern
72
+ # @param window [Numeric] Window size in seconds
73
+ def add_rate_limit_per_event(pattern, limit:, window: 1.0)
74
+ (@rate_limiting_per_event_limits ||= []) << {
75
+ pattern: pattern.to_s,
76
+ limit: limit,
77
+ window: window.to_f
78
+ }
79
+ end
80
+
81
+ # Find the most specific rate limit config for a given event name.
82
+ #
83
+ # @param event_name [String] Event name to look up
84
+ # @return [Hash] { limit:, window: }
85
+ def rate_limit_for(event_name)
86
+ limits = @rate_limiting_per_event_limits || []
87
+ match = limits.find do |rule|
88
+ pattern = rule[:pattern].gsub(".", "\\.").gsub("*", ".*")
89
+ Regexp.new("^#{pattern}$").match?(event_name.to_s)
90
+ end
91
+ m = match || { limit: @rate_limiting_per_event_limit, window: @rate_limiting_global_window }
92
+ { limit: m[:limit], window: m[:window] }
93
+ end
94
+
95
+ # Add per-controller SLO config.
96
+ def add_slo_controller(name, action: nil, &)
97
+ key = action ? "#{name}##{action}" : name
98
+ cfg = ControllerSLOConfig.new
99
+ cfg.instance_eval(&) if block_given?
100
+ (@slo_tracking_controller_configs ||= {})[key] = cfg
101
+ end
102
+
103
+ # Add per-job SLO config.
104
+ def add_slo_job(name, &)
105
+ cfg = JobSLOConfig.new
106
+ cfg.instance_eval(&) if block_given?
107
+ (@slo_tracking_job_configs ||= {})[name] = cfg
108
+ end
109
+
110
+ # Set slo_tracking enabled — accepts Boolean.
111
+ def slo_tracking=(value)
112
+ @slo_tracking_enabled = value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
113
+ end
114
+
115
+ # Filter baggage hash to only allowed keys (for tracestate/job propagation).
116
+ def filter_baggage_for_propagation(hash)
117
+ return {} if hash.nil? || !hash.is_a?(Hash)
118
+ return hash unless @security_baggage_protection_enabled
119
+
120
+ allowed = (@security_baggage_protection_allowed_keys || E11y::BAGGAGE_PROTECTION_DEFAULT_ALLOWED_KEYS).map(&:to_s)
121
+ hash.select { |k, _| allowed.include?(k.to_s) }
122
+ end
123
+
124
+ # Register an adapter instance by name.
125
+ def register_adapter(name, instance)
126
+ @adapters[name.to_sym] = instance
127
+ end
128
+
129
+ # Set the default adapter(s) used when no severity-specific mapping matches.
130
+ def default_adapters=(names)
131
+ @adapter_mapping[:default] = Array(names).map(&:to_sym)
132
+ end
133
+
134
+ # @return [Array<Symbol>] Default adapter names
135
+ def default_adapters
136
+ @adapter_mapping[:default]
137
+ end
138
+
139
+ private
140
+
141
+ def initialize_basic_config
142
+ @adapters = {}
143
+ @log_level = :info
144
+ @pipeline = E11y::Pipeline::Builder.new
145
+ @enabled = nil
146
+ @environment = nil
147
+ @service_name = nil
148
+ @enable_http_tracing = false
149
+ end
150
+
151
+ def initialize_routing_config
152
+ @adapter_mapping = default_adapter_mapping
153
+ @default_retention_period = 30.days
154
+ @routing_rules = []
155
+ @fallback_adapters = [:stdout]
156
+ end
157
+
158
+ def initialize_feature_configs
159
+ init_instrumentation_configs
160
+ init_ephemeral_buffer_configs
161
+ init_error_handling_configs
162
+ init_rate_limiting_configs
163
+ init_slo_configs
164
+ init_security_configs
165
+ init_tracing_configs
166
+ init_cardinality_configs
167
+ end
168
+
169
+ def init_instrumentation_configs
170
+ @rails_instrumentation_enabled = false
171
+ @rails_instrumentation_custom_mappings = {}
172
+ @rails_instrumentation_ignore_events = []
173
+ @logger_bridge_enabled = false
174
+ @logger_bridge_track_severities = nil
175
+ @logger_bridge_ignore_patterns = []
176
+ @sidekiq_enabled = false
177
+ @active_job_enabled = false
178
+ end
179
+
180
+ def init_ephemeral_buffer_configs
181
+ @ephemeral_buffer_enabled = false
182
+ @ephemeral_buffer_flush_on_error = true
183
+ @ephemeral_buffer_flush_on_statuses = []
184
+ @ephemeral_buffer_debug_adapters = nil
185
+ @ephemeral_buffer_job_buffer_limit = nil
186
+ end
187
+
188
+ def init_error_handling_configs
189
+ @error_handling_fail_on_error = true
190
+ @dlq_storage = nil
191
+ @dlq_filter = nil
192
+ end
193
+
194
+ def init_rate_limiting_configs
195
+ @rate_limiting_enabled = false
196
+ @rate_limiting_global_limit = 10_000
197
+ @rate_limiting_global_window = 1.0
198
+ @rate_limiting_per_event_limit = 1_000
199
+ @rate_limiting_per_event_limits = []
200
+ end
201
+
202
+ def init_slo_configs
203
+ @slo_tracking_enabled = true
204
+ @slo_tracking_http_ignore_statuses = []
205
+ @slo_tracking_latency_percentiles = [50, 95, 99]
206
+ @slo_tracking_controller_configs = {}
207
+ @slo_tracking_job_configs = {}
208
+ end
209
+
210
+ def init_security_configs
211
+ @security_baggage_protection_enabled = true
212
+ @security_baggage_protection_allowed_keys = E11y::BAGGAGE_PROTECTION_DEFAULT_ALLOWED_KEYS.dup
213
+ @security_baggage_protection_block_mode = :silent
214
+ end
215
+
216
+ def init_tracing_configs
217
+ @tracing_source = :e11y
218
+ @tracing_default_sample_rate = 0.1
219
+ @tracing_respect_parent_sampling = true
220
+ @tracing_per_event_sample_rates = {}
221
+ @tracing_always_sample_if = nil
222
+ @opentelemetry_span_creation_patterns = []
223
+ end
224
+
225
+ def init_cardinality_configs
226
+ @cardinality_protection_max_cardinality_limit = 1000
227
+ @cardinality_protection_denylist = []
228
+ @cardinality_protection_overflow_strategy = :relabel
229
+ end
230
+
231
+ def default_adapter_mapping
232
+ {
233
+ error: %i[logs errors_tracker],
234
+ fatal: %i[logs errors_tracker],
235
+ default: [:logs]
236
+ }
237
+ end
238
+
239
+ def configure_default_pipeline
240
+ @pipeline.use E11y::Middleware::TrackLatency
241
+ @pipeline.use E11y::Middleware::TraceContext
242
+ @pipeline.use E11y::Middleware::Validation
243
+ @pipeline.use E11y::Middleware::BaggageProtection
244
+ @pipeline.use E11y::Middleware::AuditSigning
245
+ @pipeline.use E11y::Middleware::PIIFilter
246
+ @pipeline.use E11y::Middleware::RateLimiting
247
+ @pipeline.use E11y::Middleware::Sampling
248
+ @pipeline.use E11y::Middleware::Versioning
249
+ @pipeline.use E11y::Middleware::Routing
250
+ @pipeline.use E11y::Middleware::EventSlo
251
+ @pipeline.use E11y::Middleware::SelfMonitoringEmit
252
+ end
253
+ end
254
+
255
+ # Per-controller SLO config (used by add_slo_controller).
256
+ class ControllerSLOConfig
257
+ def slo_target(value = nil)
258
+ value ? @slo_target = value : @slo_target
259
+ end
260
+
261
+ def latency_target(value = nil)
262
+ value ? @latency_target = value : @latency_target
263
+ end
264
+ end
265
+
266
+ # Per-job SLO config (used by add_slo_job).
267
+ class JobSLOConfig
268
+ def ignore(value = nil)
269
+ value.nil? ? @ignore : @ignore = value
270
+ end
271
+ end
272
+ end
data/lib/e11y/console.rb CHANGED
@@ -51,19 +51,17 @@ module E11y
51
51
 
52
52
  # List all registered event classes
53
53
  def events
54
- puts "📋 E11y events list"
55
- puts " (Waiting for Event registry implementation)"
56
- []
54
+ Registry.event_classes.map { |e| e.respond_to?(:event_name) ? e.event_name : e.name }
57
55
  end
58
56
 
59
- # List all registered adapters
57
+ # List all registered adapters (from config.adapters)
60
58
  def adapters
61
- Adapters::Registry.all.map do |adapter|
59
+ config.adapters.map do |name, adapter|
62
60
  {
63
- name: adapter.name,
61
+ name: name,
64
62
  class: adapter.class.name,
65
- healthy: adapter.healthy?,
66
- capabilities: adapter.capabilities
63
+ healthy: adapter.respond_to?(:healthy?) ? adapter.healthy? : true,
64
+ capabilities: adapter.respond_to?(:capabilities) ? adapter.capabilities : {}
67
65
  }
68
66
  end
69
67
  end
@@ -77,8 +75,8 @@ module E11y
77
75
  private
78
76
 
79
77
  def adapters_info
80
- Adapters::Registry.all.map do |a|
81
- { name: a.name, class: a.class.name, healthy: a.healthy? }
78
+ config.adapters.map do |name, adapter|
79
+ { name: name, class: adapter.class.name, healthy: adapter.respond_to?(:healthy?) ? adapter.healthy? : true }
82
80
  end
83
81
  end
84
82
 
@@ -95,13 +93,8 @@ module E11y
95
93
  # @return [void]
96
94
  def self.configure_for_console
97
95
  E11y.configure do |config|
98
- # Console-friendly output
99
- config.adapters&.clear
100
-
101
- # Use stdout adapter with pretty printing
102
- config.adapters&.register :stdout, E11y::Adapters::Stdout.new(
103
- colorize: true
104
- )
96
+ config.adapters.clear
97
+ config.adapters[:stdout] = E11y::Adapters::Stdout.new(colorize: true, format: :rich)
105
98
 
106
99
  # Show all severities
107
100
  # TODO: Implement severity_threshold config
data/lib/e11y/current.rb CHANGED
@@ -37,12 +37,64 @@ module E11y
37
37
  class Current < ActiveSupport::CurrentAttributes
38
38
  attribute :trace_id
39
39
  attribute :span_id
40
- attribute :parent_trace_id # ✅ NEW: Link to parent trace (C17 Resolution)
40
+ attribute :parent_trace_id # Link to parent trace (C17 Resolution)
41
+ attribute :sampled # Trace-consistent sampling (ADR-005 §7)
42
+ attribute :baggage # Key-value metadata for cross-service propagation (ADR-005 §3)
41
43
  attribute :request_id
42
44
  attribute :user_id
43
45
  attribute :ip_address
44
46
  attribute :user_agent
45
47
  attribute :request_method
46
48
  attribute :request_path
49
+
50
+ # Add baggage key-value (propagated via tracestate / job metadata).
51
+ # Respects config.security_baggage_protection_*: blocks PII keys per allowed_keys (ADR-006 §5.5).
52
+ #
53
+ # @param key [String, Symbol]
54
+ # @param value [Object] Converted to string
55
+ # @raise [E11y::BaggagePiiError] when block_mode is :raise and key not allowed
56
+ def self.add_baggage(key, value)
57
+ cfg = E11y.config
58
+ if cfg&.security_baggage_protection_enabled
59
+ allowed = (cfg.security_baggage_protection_allowed_keys || E11y::BAGGAGE_PROTECTION_DEFAULT_ALLOWED_KEYS).map(&:to_s)
60
+ unless allowed.include?(key.to_s)
61
+ message = "[E11y] Blocked PII from E11y baggage: key=#{key.inspect}"
62
+ case cfg.security_baggage_protection_block_mode || :silent
63
+ when :silent then E11y.logger&.debug(message)
64
+ when :warn then E11y.logger&.warn(message)
65
+ when :raise then raise E11y::BaggagePiiError, "#{message}. Only allowed keys: #{allowed.join(', ')}"
66
+ end
67
+ return
68
+ end
69
+ end
70
+ self.baggage = (baggage || {}).merge(key.to_s => value.to_s)
71
+ end
72
+
73
+ # Get baggage value by key.
74
+ # @param key [String, Symbol]
75
+ # @return [String, nil]
76
+ def self.get_baggage(key)
77
+ baggage&.[](key.to_s)
78
+ end
79
+
80
+ # Returns current attributes as a hash for sampling context (symbol keys, nil values omitted).
81
+ # Callers may merge job-specific keys (job_class, queue) when in job context.
82
+ #
83
+ # @return [Hash{Symbol=>Object}]
84
+ def self.to_context
85
+ {
86
+ trace_id: trace_id,
87
+ span_id: span_id,
88
+ parent_trace_id: parent_trace_id,
89
+ sampled: sampled,
90
+ baggage: baggage,
91
+ request_id: request_id,
92
+ user_id: user_id,
93
+ ip_address: ip_address,
94
+ user_agent: user_agent,
95
+ request_method: request_method,
96
+ request_path: request_path
97
+ }.compact
98
+ end
47
99
  end
48
100
  end