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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/middleware/base"
4
+ require "e11y/slo/config_loader"
5
+
6
+ module E11y
7
+ module Middleware
8
+ # SelfMonitoringEmit middleware — emits e11y_events_tracked_total at pipeline end.
9
+ #
10
+ # When e11y_self_monitoring.enabled is true in slo.yml, increments the counter
11
+ # for each event that reaches the end of the pipeline (after EventSlo).
12
+ #
13
+ # **Middleware Zone:** `:post_processing` (last in pipeline)
14
+ #
15
+ # @example slo.yml
16
+ # e11y_self_monitoring:
17
+ # enabled: true
18
+ # targets:
19
+ # reliability: 0.999
20
+ #
21
+ # @see docs/plans/2026-03-13-slo-linters-self-monitoring-plan.md
22
+ class SelfMonitoringEmit < Base
23
+ middleware_zone :post_processing
24
+
25
+ # Process event and optionally emit self-monitoring metric.
26
+ #
27
+ # @param event_data [Hash, nil] Event payload (nil passes through)
28
+ # @return [Hash, nil] Unchanged event_data (passthrough)
29
+ def call(event_data)
30
+ if event_data && E11y::SLO::ConfigLoader.self_monitoring_enabled?
31
+ event_name = event_data[:event_name].to_s.presence || "unknown"
32
+ E11y::Metrics.increment(:e11y_events_tracked_total, result: "success", event_name: event_name)
33
+ end
34
+
35
+ @app&.call(event_data) || event_data
36
+ end
37
+ end
38
+ end
39
+ end
@@ -56,19 +56,19 @@ module E11y
56
56
  def call(event_data)
57
57
  enrich_trace_context(event_data)
58
58
  enrich_service_context(event_data)
59
- increment_metric("e11y.middleware.trace_context.processed")
59
+ E11y::Metrics.increment("e11y.middleware.trace_context.processed")
60
60
  @app.call(event_data)
61
61
  end
62
62
 
63
63
  private
64
64
 
65
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
65
+ # rubocop:disable Metrics/AbcSize
66
66
  # Add distributed tracing fields to event data
67
67
  # @param event_data [Hash] Event data to enrich
68
68
  # @return [void]
69
69
  def enrich_trace_context(event_data)
70
70
  event_data[:trace_id] ||= current_trace_id || generate_trace_id
71
- event_data[:span_id] ||= generate_span_id
71
+ event_data[:span_id] ||= current_span_id || generate_span_id
72
72
  event_data[:parent_trace_id] ||= current_parent_trace_id if current_parent_trace_id
73
73
 
74
74
  # Format timestamp if it's a Time object
@@ -93,7 +93,7 @@ module E11y
93
93
 
94
94
  event_data[:audit_event] = event_class.audit_event?
95
95
  end
96
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
96
+ # rubocop:enable Metrics/AbcSize
97
97
 
98
98
  # Add service context fields to event data
99
99
  # @param event_data [Hash] Event data to enrich
@@ -103,15 +103,54 @@ module E11y
103
103
  event_data[:environment] ||= E11y.config.environment
104
104
  end
105
105
 
106
- # Get current trace ID from E11y::Current or thread-local storage (request context).
106
+ # Get current trace ID from configured source (ADR-007 §8).
107
107
  #
108
- # Priority: E11y::Current > Thread.current
108
+ # When config.tracing_source is :opentelemetry and OTel SDK has an active span,
109
+ # uses trace_id from OpenTelemetry::Trace.current_span.
110
+ # Otherwise: E11y::Current > Thread.current
109
111
  #
110
112
  # @return [String, nil] Current trace ID if set, nil otherwise
111
113
  def current_trace_id
114
+ if tracing_source_opentelemetry?
115
+ otel = otel_trace_context
116
+ return otel[:trace_id] if otel[:trace_id]
117
+ end
112
118
  E11y::Current.trace_id || Thread.current[:e11y_trace_id]
113
119
  end
114
120
 
121
+ # Get current span ID (for event correlation).
122
+ # When using OTel source and span exists, returns OTel span_id; otherwise nil (caller generates).
123
+ #
124
+ # @return [String, nil]
125
+ def current_span_id
126
+ return nil unless tracing_source_opentelemetry?
127
+
128
+ otel = otel_trace_context
129
+ otel[:span_id]
130
+ end
131
+
132
+ def tracing_source_opentelemetry?
133
+ E11y.config&.tracing_source == :opentelemetry
134
+ end
135
+
136
+ def otel_trace_context
137
+ return {} unless defined?(OpenTelemetry::Trace)
138
+
139
+ span = OpenTelemetry::Trace.current_span
140
+ ctx = span.context
141
+ return {} unless ctx.respond_to?(:valid?) && ctx.valid?
142
+
143
+ trace_id = ctx.respond_to?(:hex_trace_id) ? ctx.hex_trace_id : nil
144
+ span_id = ctx.respond_to?(:hex_span_id) ? ctx.hex_span_id : nil
145
+ return {} if trace_id.to_s.empty?
146
+
147
+ # Sync to E11y::Current so downstream uses same context
148
+ E11y::Current.trace_id = trace_id
149
+ E11y::Current.span_id = span_id
150
+
151
+ { trace_id: trace_id, span_id: span_id }
152
+ end
153
+
115
154
  # Get current parent trace ID from E11y::Current (background job context).
116
155
  #
117
156
  # Only set for background jobs that have a parent request trace.
@@ -151,10 +190,6 @@ module E11y
151
190
  #
152
191
  # @param metric_name [String] Metric name
153
192
  # @return [void]
154
- def increment_metric(_metric_name)
155
- # TODO: Integrate with Yabeda/Prometheus in Phase 2
156
- # Yabeda.e11y.middleware_trace_context_processed.increment
157
- end
158
193
  end
159
194
  end
160
195
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module Middleware
5
+ # Measures Event.track() latency from pipeline entry to exit.
6
+ #
7
+ # Must be the FIRST middleware so it wraps the entire pipeline.
8
+ # Records duration for both success and dropped events.
9
+ #
10
+ # @see ADR-016 §3.1 (Performance Metrics)
11
+ # @example Add first in pipeline
12
+ # config.pipeline.use E11y::Middleware::TrackLatency
13
+ # config.pipeline.use E11y::Middleware::TraceContext
14
+ # # ...
15
+ class TrackLatency < Base
16
+ middleware_zone :pre_processing
17
+
18
+ def call(event_data)
19
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ result = @app.call(event_data)
21
+ duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
22
+
23
+ E11y::SelfMonitoring::PerformanceMonitor.track_latency(
24
+ duration_ms,
25
+ event_class: event_data[:event_name].to_s,
26
+ severity: event_data[:severity].to_s,
27
+ result: result.nil? ? :dropped : :success
28
+ )
29
+
30
+ result
31
+ end
32
+ end
33
+ end
34
+ end
@@ -56,7 +56,7 @@ module E11y
56
56
  # @option event_data [Hash] :payload The event payload (required)
57
57
  # @return [Hash, nil] Validated event data, or nil if dropped
58
58
  # @raise [E11y::ValidationError] if validation fails
59
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
59
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
60
60
  def call(event_data)
61
61
  # Skip validation if no event_class or payload
62
62
  return @app.call(event_data) unless event_data[:event_class] && event_data[:payload]
@@ -69,7 +69,7 @@ module E11y
69
69
 
70
70
  # Skip validation if mode is :never
71
71
  if validation_mode == :never
72
- increment_metric("e11y.middleware.validation.skipped")
72
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
73
73
  return @app.call(event_data)
74
74
  end
75
75
 
@@ -77,7 +77,7 @@ module E11y
77
77
  if validation_mode == :sampled
78
78
  sample_rate = event_class.respond_to?(:validation_sample_rate) ? event_class.validation_sample_rate : 0.01
79
79
  if rand >= sample_rate
80
- increment_metric("e11y.middleware.validation.skipped")
80
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
81
81
  return @app.call(event_data)
82
82
  end
83
83
  end
@@ -87,7 +87,7 @@ module E11y
87
87
 
88
88
  # Skip validation if no schema defined (schema-less events)
89
89
  unless schema
90
- increment_metric("e11y.middleware.validation.skipped")
90
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
91
91
  return @app.call(event_data)
92
92
  end
93
93
 
@@ -96,17 +96,17 @@ module E11y
96
96
 
97
97
  if result.success?
98
98
  # Validation passed
99
- increment_metric("e11y.middleware.validation.passed")
99
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "passed")
100
100
  @app.call(event_data)
101
101
  else
102
102
  # Validation failed - raise error with details
103
- increment_metric("e11y.middleware.validation.failed")
103
+ E11y::Metrics.increment(:e11y_middleware_validation_total, result: "failed")
104
104
 
105
105
  error_message = format_validation_errors(event_class, result.errors)
106
106
  raise E11y::ValidationError, error_message
107
107
  end
108
108
  end
109
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
109
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
110
110
 
111
111
  private
112
112
 
@@ -122,15 +122,6 @@ module E11y
122
122
 
123
123
  "Validation failed for #{event_class.name}: #{error_details}"
124
124
  end
125
-
126
- # Placeholder for metrics instrumentation.
127
- #
128
- # @param metric_name [String] Metric name
129
- # @return [void]
130
- def increment_metric(_metric_name)
131
- # TODO: Integrate with Yabeda/Prometheus in Phase 2
132
- # Yabeda.e11y.middleware_validation_passed.increment
133
- end
134
125
  end
135
126
  end
136
127
  end
@@ -54,43 +54,47 @@ module E11y
54
54
  # @see ADR-012 for versioning architecture
55
55
  # @see UC-020 for use cases
56
56
  class Versioning < Base
57
- # Version extraction regex (matches V2, V3, etc. at end of class name)
58
- VERSION_REGEX = /V(\d+)$/
57
+ middleware_zone :pre_processing
58
+ VERSION_REGEX = E11y::Versioning::VersionExtractor::VERSION_REGEX
59
+
60
+ # Lazy cache: class name -> normalized event_name (per class, immutable)
61
+ NORMALIZED_CACHE = Concurrent::Map.new
59
62
 
60
63
  # Process event and add version field if needed
61
64
  #
62
65
  # @param event_data [Hash] Event payload
63
66
  # @return [Hash] Event data with version field (if > 1)
64
67
  def call(event_data)
65
- # Extract version from event_name (class name)
66
- version = extract_version(event_data[:event_name])
68
+ klass = event_data[:event_class]
69
+ class_name = klass&.name
67
70
 
68
- # Add version field only if > 1 (ADR-012 §4.2)
71
+ version = event_data[:version].to_i
72
+ version = extract_version(class_name) if version <= 1
69
73
  event_data[:v] = version if version > 1
70
74
 
71
- # Normalize event_name (remove version suffix for consistent queries)
72
- event_data[:event_name] = normalize_event_name(event_data[:event_name])
75
+ # event_data[:event_name] set by Base; fallback to klass.event_name for minimal event_data (tests)
76
+ incoming = event_data[:event_name]
77
+ incoming = klass.event_name if incoming.nil? && klass.respond_to?(:event_name)
78
+ incoming = incoming.to_s
79
+ # Custom uses dot notation ("order.paid"); default from Base uses "::"
80
+ event_data[:event_name] = incoming != "" && !incoming.include?("::") ? incoming : normalized_for(klass)
73
81
 
74
- event_data
82
+ @app&.call(event_data) || event_data
75
83
  end
76
84
 
77
85
  private
78
86
 
79
- # Extract version from event class name
80
- #
81
- # @param class_name [String] Event class name (e.g., "Events::OrderPaidV2")
82
- # @return [Integer] Version number (default: 1)
83
- #
84
- # @example
85
- # extract_version("Events::OrderPaid") => 1
86
- # extract_version("Events::OrderPaidV2") => 2
87
- # extract_version("Events::OrderPaidV3") => 3
88
- def extract_version(class_name)
89
- return 1 unless class_name
87
+ def normalized_for(klass)
88
+ return unless klass
90
89
 
91
- # Extract version from class name (e.g., "Events::OrderPaidV2" → 2)
92
- match = class_name.match(VERSION_REGEX)
93
- match ? match[1].to_i : 1
90
+ name = klass.name
91
+ return unless name
92
+
93
+ NORMALIZED_CACHE.fetch(name) { NORMALIZED_CACHE[name] = normalize_event_name(name) }
94
+ end
95
+
96
+ def extract_version(class_name)
97
+ E11y::Versioning::VersionExtractor.extract_version(class_name)
94
98
  end
95
99
 
96
100
  # Normalize event_name by removing version suffix
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module E11y
4
+ module OpenTelemetry
5
+ # Semantic conventions mapper for OTel attributes (ADR-007 §4, F4).
6
+ #
7
+ # Maps E11y payload keys to OpenTelemetry semantic convention attribute names.
8
+ # When event_name matches a convention type (http, database, etc.), known keys
9
+ # are mapped to semantic names (e.g. method → http.method).
10
+ #
11
+ # @see https://opentelemetry.io/docs/specs/semconv/
12
+ class SemanticConventions
13
+ # Key mappings by convention type
14
+ # https://opentelemetry.io/docs/specs/semconv/http/
15
+ # https://opentelemetry.io/docs/specs/semconv/database/
16
+ # https://opentelemetry.io/docs/specs/semconv/exceptions/
17
+ CONVENTIONS = {
18
+ http: {
19
+ "method" => "http.method",
20
+ "route" => "http.route",
21
+ "path" => "http.target",
22
+ "status_code" => "http.status_code",
23
+ "status" => "http.status_code",
24
+ "duration_ms" => "http.server.duration",
25
+ "request_size" => "http.request.body.size",
26
+ "response_size" => "http.response.body.size",
27
+ "user_agent" => "http.user_agent",
28
+ "client_ip" => "http.client_ip",
29
+ "scheme" => "http.scheme",
30
+ "host" => "http.host",
31
+ "server_name" => "http.server_name"
32
+ },
33
+ database: {
34
+ "query" => "db.statement",
35
+ "statement" => "db.statement",
36
+ "duration_ms" => "db.operation.duration",
37
+ "rows_affected" => "db.operation.rows_affected",
38
+ "connection_id" => "db.connection.id",
39
+ "database_name" => "db.name",
40
+ "table_name" => "db.sql.table",
41
+ "operation" => "db.operation"
42
+ },
43
+ rpc: {
44
+ "service" => "rpc.service",
45
+ "method" => "rpc.method",
46
+ "system" => "rpc.system",
47
+ "status_code" => "rpc.grpc.status_code"
48
+ },
49
+ messaging: {
50
+ "queue_name" => "messaging.destination.name",
51
+ "message_id" => "messaging.message.id",
52
+ "conversation_id" => "messaging.message.conversation_id",
53
+ "payload_size" => "messaging.message.payload_size_bytes",
54
+ "operation" => "messaging.operation"
55
+ },
56
+ exception: {
57
+ "error_type" => "exception.type",
58
+ "error_message" => "exception.message",
59
+ "error_class" => "exception.type",
60
+ "stacktrace" => "exception.stacktrace"
61
+ }
62
+ }.freeze
63
+
64
+ # Map payload keys to OTel semantic attribute names.
65
+ #
66
+ # @param event_name [String] Event name (used to detect convention type)
67
+ # @param payload [Hash] Event payload
68
+ # @return [Hash] Mapped payload with semantic keys where applicable
69
+ def self.map(event_name, payload)
70
+ convention_type = detect_convention_type(event_name)
71
+ return payload.transform_keys { |k| "event.#{k}" } unless convention_type
72
+
73
+ conventions = CONVENTIONS[convention_type]
74
+ payload.each_with_object({}) do |(key, value), mapped|
75
+ otel_key = conventions[key.to_s] || "event.#{key}"
76
+ mapped[otel_key] = value
77
+ end
78
+ end
79
+
80
+ # Map a single key to OTel semantic attribute name.
81
+ #
82
+ # @param event_name [String] Event name (used to detect convention type)
83
+ # @param key [String, Symbol] Payload key
84
+ # @return [String] OTel attribute key
85
+ def self.map_key(event_name, key)
86
+ convention_type = detect_convention_type(event_name)
87
+ return "event.#{key}" unless convention_type
88
+
89
+ conventions = CONVENTIONS[convention_type]
90
+ conventions[key.to_s] || "event.#{key}"
91
+ end
92
+
93
+ # Detect convention type from event name
94
+ #
95
+ # @param event_name [String]
96
+ # @return [Symbol, nil]
97
+ def self.detect_convention_type(event_name)
98
+ name = event_name.to_s
99
+ return :http if name.match?(/http|request|response/i)
100
+ return :database if name.match?(/database|query|sql|postgres|mysql/i)
101
+ return :rpc if name.match?(/rpc|grpc/i)
102
+ return :messaging if name.match?(/message|queue|kafka|rabbitmq|sidekiq|job/i)
103
+ return :exception if name.match?(/error|exception|failure/i)
104
+
105
+ nil
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "e11y/opentelemetry/semantic_conventions"
4
+
5
+ module E11y
6
+ module OpenTelemetry
7
+ # Creates OpenTelemetry spans from E11y events (ADR-007 §6, F2).
8
+ #
9
+ # When enabled via config.opentelemetry_span_creation_patterns, creates
10
+ # OTel spans for matching events. Errors/fatal always create spans.
11
+ # Uses SemanticConventions for attribute mapping when applicable.
12
+ #
13
+ # @example Configuration
14
+ # E11y.configure do |config|
15
+ # config.opentelemetry_span_creation_patterns = ["order.*", "payment.*"]
16
+ # end
17
+ #
18
+ # @see ADR-007 §6 Traces Signal Export
19
+ # @see E11y::OpenTelemetry::SemanticConventions
20
+ class SpanCreator
21
+ ATTR_EVENT_NAME = "event.name"
22
+ ATTR_SEVERITY = "event.severity"
23
+ ATTR_E11Y_TRACE_ID = "e11y.trace_id"
24
+ ATTR_E11Y_SPAN_ID = "e11y.span_id"
25
+
26
+ class << self
27
+ def create_span_from_event(event_data)
28
+ return unless defined?(::OpenTelemetry::Trace)
29
+ return unless should_create_span?(event_data)
30
+
31
+ tracer = ::OpenTelemetry.tracer_provider.tracer("e11y", E11y::VERSION)
32
+ parent_ctx = ::OpenTelemetry::Context.current
33
+ start_ts = time_to_nano(event_data[:timestamp] || Time.now)
34
+
35
+ span = tracer.start_span(
36
+ span_name(event_data),
37
+ with_parent: parent_ctx,
38
+ kind: span_kind(event_data),
39
+ start_timestamp: start_ts
40
+ )
41
+
42
+ set_attributes(span, event_data)
43
+ set_status(span, event_data)
44
+ record_exception(span, event_data) if event_data[:severity].in?(%i[error fatal])
45
+
46
+ end_ts = compute_end_timestamp(event_data)
47
+ span.finish(end_timestamp: end_ts)
48
+
49
+ span
50
+ end
51
+
52
+ private
53
+
54
+ def span_name(event_data)
55
+ event_data[:event_name].to_s.presence || "e11y.event"
56
+ end
57
+
58
+ def set_attributes(span, event_data)
59
+ span.set_attribute(ATTR_EVENT_NAME, event_data[:event_name].to_s)
60
+ span.set_attribute(ATTR_SEVERITY, event_data[:severity].to_s)
61
+ span.set_attribute(ATTR_E11Y_TRACE_ID, event_data[:trace_id].to_s) if event_data[:trace_id]
62
+ span.set_attribute(ATTR_E11Y_SPAN_ID, event_data[:span_id].to_s) if event_data[:span_id]
63
+
64
+ payload = event_data[:payload] || {}
65
+ return if payload.empty?
66
+
67
+ mapped = E11y::OpenTelemetry::SemanticConventions.map(event_data[:event_name].to_s, payload)
68
+ mapped.each do |key, value|
69
+ next if value.nil?
70
+
71
+ span.set_attribute(key.to_s, otel_value(value))
72
+ rescue ArgumentError, TypeError
73
+ span.set_attribute(key.to_s, value.to_s)
74
+ end
75
+ end
76
+
77
+ def otel_value(value)
78
+ case value
79
+ when TrueClass, FalseClass, Integer, Float, String then value
80
+ when Array then value.map(&:to_s)
81
+ else value.to_s # Symbol, NilClass, Hash, etc.
82
+ end
83
+ end
84
+
85
+ def set_status(span, event_data)
86
+ if event_data[:severity].in?(%i[error fatal])
87
+ msg = event_data.dig(:payload, :error_message) ||
88
+ event_data.dig(:payload, "error_message") || "Error"
89
+ span.status = ::OpenTelemetry::Trace::Status.error(msg.to_s)
90
+ else
91
+ span.status = ::OpenTelemetry::Trace::Status.ok
92
+ end
93
+ end
94
+
95
+ def record_exception(span, event_data)
96
+ exc = event_data[:exception] || event_data.dig(:payload, :exception) || event_data.dig(:payload, "exception")
97
+ span.record_exception(exc) if exc.is_a?(Exception)
98
+ end
99
+
100
+ def compute_end_timestamp(event_data)
101
+ start = event_data[:timestamp] || Time.now
102
+ start_ns = time_to_nano(start)
103
+ if event_data[:duration_ms]
104
+ start_ns + (event_data[:duration_ms].to_f * 1_000_000).to_i
105
+ else
106
+ time_to_nano(Time.now)
107
+ end
108
+ end
109
+
110
+ def should_create_span?(event_data)
111
+ return true if event_data[:severity].in?(%i[error fatal])
112
+
113
+ patterns = E11y.config&.opentelemetry_span_creation_patterns || []
114
+ event_name = event_data[:event_name].to_s
115
+ return false if event_name.empty?
116
+
117
+ patterns.any? { |p| File.fnmatch(p.to_s, event_name) }
118
+ end
119
+
120
+ def span_kind(event_data)
121
+ kind = (event_data[:span_kind] || :internal).to_sym
122
+ case kind
123
+ when :server then ::OpenTelemetry::Trace::SpanKind::SERVER
124
+ when :client then ::OpenTelemetry::Trace::SpanKind::CLIENT
125
+ when :producer then ::OpenTelemetry::Trace::SpanKind::PRODUCER
126
+ when :consumer then ::OpenTelemetry::Trace::SpanKind::CONSUMER
127
+ else ::OpenTelemetry::Trace::SpanKind::INTERNAL
128
+ end
129
+ rescue StandardError
130
+ ::OpenTelemetry::Trace::SpanKind::INTERNAL
131
+ end
132
+
133
+ def time_to_nano(time)
134
+ return (Time.now.to_f * 1_000_000_000).to_i if time.nil?
135
+
136
+ time = Time.parse(time.to_s) if time.is_a?(String)
137
+ (time.to_f * 1_000_000_000).to_i
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -15,7 +15,7 @@ module E11y
15
15
  EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/
16
16
 
17
17
  # Password-like field names
18
- PASSWORD_FIELDS = /password|passwd|pwd|secret|token|api[_-]?key/i
18
+ PASSWORD_FIELDS = /\b(?:password|passwd|pwd|secret|token|api[_-]?key)\b/i
19
19
 
20
20
  # Social Security Number (US format: XXX-XX-XXXX)
21
21
  SSN = /\b\d{3}-\d{2}-\d{4}\b/
@@ -40,6 +40,17 @@ module E11y
40
40
  PHONE
41
41
  ].freeze
42
42
 
43
+ # Patterns applied to STRING VALUES only (excludes PASSWORD_FIELDS).
44
+ # PASSWORD_FIELDS matches field names (password, token, api_key), not values.
45
+ # Applying it to values corrupts legitimate strings like "process_token_renewal_completed".
46
+ VALUE_PATTERNS = [
47
+ EMAIL,
48
+ SSN,
49
+ CREDIT_CARD,
50
+ IPV4,
51
+ PHONE
52
+ ].freeze
53
+
43
54
  # Field name patterns that indicate PII
44
55
  # Used for field-level detection (case-insensitive)
45
56
  FIELD_PATTERNS = {
@@ -33,7 +33,7 @@ module E11y
33
33
  # @see ADR-015 §3.4 Middleware Zones & Modification Rules
34
34
  class Builder
35
35
  # Middleware entry: [middleware_class, args, options]
36
- MiddlewareEntry = Struct.new(:middleware_class, :args, :options, keyword_init: true)
36
+ MiddlewareEntry = Struct.new(:middleware_class, :args, :options)
37
37
 
38
38
  # @return [Array<MiddlewareEntry>] Registered middlewares
39
39
  attr_reader :middlewares
@@ -38,11 +38,13 @@ module E11y
38
38
  module AuditEvent
39
39
  def self.included(base)
40
40
  base.class_eval do
41
- # Audit events will use audit pipeline (Phase 4)
41
+ audit_event true
42
+ contains_pii false # Preserve all data for signing (Tier 1 = skip filtering)
43
+ use_dlq true # Audit events always saved to DLQ (compliance)
42
44
  # Severity is NOT set by preset - user decides based on event criticality
43
45
  end
44
46
 
45
- # Extend class with audit-specific methods
47
+ # Extend class with audit-specific methods (resolve_sample_rate 1.0, resolve_rate_limit nil)
46
48
  base.extend(ClassMethods)
47
49
  end
48
50
 
@@ -59,6 +61,15 @@ module E11y
59
61
  def resolve_sample_rate
60
62
  1.0 # 100% - compliance requirement
61
63
  end
64
+
65
+ # Audit events use routing rules (UC-012), not severity-based adapters.
66
+ # Return [] when no explicit adapters; respect explicit adapters when set.
67
+ def adapters(*list)
68
+ @adapters = list.flatten if list.any?
69
+ return @adapters if @adapters
70
+
71
+ []
72
+ end
62
73
  end
63
74
  end
64
75
  end