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.
- checksums.yaml +4 -4
- data/.rubocop.yml +130 -10
- data/CHANGELOG.md +56 -1
- data/CLAUDE.md +168 -0
- data/CONTRIBUTING.md +640 -0
- data/README.md +134 -702
- data/RELEASE.md +18 -3
- data/Rakefile +108 -29
- data/config/README.md +1 -1
- data/config/loki-local-config.yaml +12 -0
- data/config/otel-collector-config.yaml +44 -0
- data/cucumber.yml +1 -0
- data/docker-compose.yml +18 -2
- data/docs/ADAPTERS.md +76 -0
- data/docs/ADAPTIVE_SAMPLING.md +59 -0
- data/docs/COMPARISON.md +104 -0
- data/docs/CONFIGURATION.md +52 -0
- data/docs/DISTRIBUTED_TRACING.md +44 -0
- data/docs/LIMITATIONS.md +13 -0
- data/docs/METRICS_DSL.md +84 -0
- data/docs/PERFORMANCE.md +60 -0
- data/docs/PII_FILTERING.md +40 -0
- data/docs/PRESETS.md +65 -0
- data/docs/QUICK-START.md +546 -587
- data/docs/RAILS_INTEGRATION.md +29 -0
- data/docs/SCHEMA_VALIDATION.md +63 -0
- data/docs/SLO-PROMQL-ALERTS.md +161 -0
- data/docs/TESTING.md +69 -0
- data/docs/{ADR-001-architecture.md → architecture/ADR-001-architecture.md} +35 -64
- data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
- data/docs/{ADR-003-slo-observability.md → architecture/ADR-003-slo-observability.md} +27 -466
- data/docs/{ADR-004-adapter-architecture.md → architecture/ADR-004-adapter-architecture.md} +163 -146
- data/docs/{ADR-005-tracing-context.md → architecture/ADR-005-tracing-context.md} +10 -9
- data/docs/{ADR-006-security-compliance.md → architecture/ADR-006-security-compliance.md} +184 -191
- data/docs/{ADR-007-opentelemetry-integration.md → architecture/ADR-007-opentelemetry-integration.md} +3 -21
- data/docs/{ADR-008-rails-integration.md → architecture/ADR-008-rails-integration.md} +209 -339
- data/docs/{ADR-009-cost-optimization.md → architecture/ADR-009-cost-optimization.md} +45 -54
- data/docs/architecture/ADR-010-developer-experience.md +522 -0
- data/docs/{ADR-011-testing-strategy.md → architecture/ADR-011-testing-strategy.md} +41 -83
- data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
- data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
- data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +23 -41
- data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +52 -349
- data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
- data/docs/architecture/ADR-018-memory-optimization.md +366 -0
- data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
- data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
- data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
- data/docs/prd/01-overview-vision.md +19 -14
- data/docs/use_cases/README.md +22 -23
- data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
- data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
- data/docs/use_cases/UC-003-event-metrics.md +66 -0
- data/docs/use_cases/UC-004-zero-config-slo-tracking.md +42 -101
- data/docs/use_cases/UC-005-sentry-integration.md +13 -15
- data/docs/use_cases/UC-006-trace-context-management.md +30 -28
- data/docs/use_cases/UC-007-pii-filtering.md +35 -87
- data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
- data/docs/use_cases/UC-009-multi-service-tracing.md +4 -4
- data/docs/use_cases/UC-010-background-job-tracking.md +5 -5
- data/docs/use_cases/UC-011-rate-limiting.md +95 -168
- data/docs/use_cases/UC-012-audit-trail.md +21 -46
- data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
- data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
- data/docs/use_cases/UC-015-cost-optimization.md +46 -99
- data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
- data/docs/use_cases/UC-017-local-development.md +203 -777
- data/docs/use_cases/UC-018-testing-events.md +3 -3
- data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
- data/docs/use_cases/UC-020-event-versioning.md +8 -9
- data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
- data/docs/use_cases/UC-022-event-registry.md +15 -21
- data/docs/use_cases/backlog.md +119 -87
- data/e11y.gemspec +2 -2
- data/gems/e11y-devtools/README.md +136 -0
- data/gems/e11y-devtools/config/routes.rb +8 -0
- data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
- data/gems/e11y-devtools/exe/e11y +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +115 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +54 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
- data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
- data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
- data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +58 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
- data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
- data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
- data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
- data/lib/e11y/adapters/audit_encrypted.rb +53 -11
- data/lib/e11y/adapters/base.rb +33 -34
- data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
- data/lib/e11y/adapters/dev_log/query.rb +219 -0
- data/lib/e11y/adapters/dev_log.rb +118 -0
- data/lib/e11y/adapters/file.rb +3 -6
- data/lib/e11y/adapters/in_memory.rb +52 -5
- data/lib/e11y/adapters/in_memory_test.rb +29 -0
- data/lib/e11y/adapters/loki.rb +58 -23
- data/lib/e11y/adapters/null.rb +82 -0
- data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
- data/lib/e11y/adapters/otel_logs.rb +136 -23
- data/lib/e11y/adapters/sentry.rb +4 -7
- data/lib/e11y/adapters/stdout.rb +73 -7
- data/lib/e11y/adapters/yabeda.rb +153 -29
- data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
- data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
- data/lib/e11y/buffers/ring_buffer.rb +3 -16
- data/lib/e11y/configuration.rb +272 -0
- data/lib/e11y/console.rb +10 -17
- data/lib/e11y/current.rb +53 -1
- data/lib/e11y/debug/pipeline_inspector.rb +96 -0
- data/lib/e11y/documentation/generator.rb +48 -0
- data/lib/e11y/event/base.rb +176 -82
- data/lib/e11y/event/value_sampling_config.rb +1 -5
- data/lib/e11y/events/rails/database/query.rb +1 -4
- data/lib/e11y/events/rails/job/failed.rb +2 -0
- data/lib/e11y/instruments/active_job.rb +46 -12
- data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
- data/lib/e11y/instruments/sidekiq.rb +137 -31
- data/lib/e11y/linters/base.rb +11 -0
- data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
- data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
- data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
- data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
- data/lib/e11y/logger/bridge.rb +26 -7
- data/lib/e11y/metrics/cardinality_protection.rb +10 -15
- data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
- data/lib/e11y/metrics/registry.rb +3 -5
- data/lib/e11y/metrics/test_backend.rb +62 -0
- data/lib/e11y/metrics.rb +56 -10
- data/lib/e11y/middleware/adapter_resolver.rb +40 -0
- data/lib/e11y/middleware/audit_signing.rb +43 -6
- data/lib/e11y/middleware/baggage_protection.rb +75 -0
- data/lib/e11y/middleware/dev_log_source.rb +24 -0
- data/lib/e11y/middleware/event_slo.rb +23 -9
- data/lib/e11y/middleware/otel_span.rb +23 -0
- data/lib/e11y/middleware/pii_filter.rb +104 -75
- data/lib/e11y/middleware/rate_limiting.rb +54 -27
- data/lib/e11y/middleware/request.rb +70 -23
- data/lib/e11y/middleware/routing.rb +78 -21
- data/lib/e11y/middleware/sampling.rb +66 -17
- data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
- data/lib/e11y/middleware/trace_context.rb +45 -10
- data/lib/e11y/middleware/track_latency.rb +34 -0
- data/lib/e11y/middleware/validation.rb +7 -16
- data/lib/e11y/middleware/versioning.rb +26 -22
- data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
- data/lib/e11y/opentelemetry/span_creator.rb +142 -0
- data/lib/e11y/pii/patterns.rb +12 -1
- data/lib/e11y/pipeline/builder.rb +1 -1
- data/lib/e11y/presets/audit_event.rb +13 -2
- data/lib/e11y/railtie.rb +52 -15
- data/lib/e11y/registry.rb +306 -0
- data/lib/e11y/reliability/circuit_breaker.rb +19 -21
- data/lib/e11y/reliability/dlq/base.rb +71 -0
- data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
- data/lib/e11y/reliability/dlq/filter.rb +37 -54
- data/lib/e11y/reliability/retry_handler.rb +26 -29
- data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
- data/lib/e11y/sampling/error_spike_detector.rb +0 -2
- data/lib/e11y/sampling/load_monitor.rb +5 -9
- data/lib/e11y/sampling/stratified_tracker.rb +18 -0
- data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
- data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
- data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
- data/lib/e11y/slo/config_loader.rb +40 -0
- data/lib/e11y/slo/config_validator.rb +58 -0
- data/lib/e11y/slo/dashboard_generator.rb +122 -0
- data/lib/e11y/slo/event_driven.rb +8 -0
- data/lib/e11y/slo/tracker.rb +31 -4
- data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
- data/lib/e11y/testing/rspec_matchers.rb +21 -0
- data/lib/e11y/testing/snapshot_matcher.rb +86 -0
- data/lib/e11y/trace_context/sampler.rb +35 -0
- data/lib/e11y/tracing/faraday_middleware.rb +31 -0
- data/lib/e11y/tracing/net_http_patch.rb +33 -0
- data/lib/e11y/tracing/propagator.rb +116 -0
- data/lib/e11y/tracing.rb +47 -0
- data/lib/e11y/version.rb +1 -1
- data/lib/e11y/versioning/version_extractor.rb +32 -0
- data/lib/e11y.rb +141 -265
- data/lib/generators/e11y/event/event_generator.rb +22 -0
- data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
- data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
- data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
- data/lib/generators/e11y/install/install_generator.rb +34 -0
- data/lib/generators/e11y/install/templates/e11y.rb +239 -0
- data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
- data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
- data/lib/tasks/e11y_docs.rake +30 -0
- data/lib/tasks/e11y_events.rake +71 -0
- data/lib/tasks/e11y_lint.rake +91 -0
- data/lib/tasks/e11y_slo.rake +29 -0
- metadata +129 -39
- data/docs/ADR-010-developer-experience.md +0 -2166
- data/docs/API-REFERENCE-L28.md +0 -914
- data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
- data/docs/CONTRIBUTING.md +0 -312
- data/docs/IMPLEMENTATION_NOTES.md +0 -2804
- data/docs/IMPLEMENTATION_PLAN.md +0 -1971
- data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
- data/docs/PLAN.md +0 -148
- data/docs/README.md +0 -296
- data/docs/design/00-memory-optimization.md +0 -593
- data/docs/guides/MIGRATION-L27-L28.md +0 -692
- data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
- data/docs/guides/README.md +0 -44
- data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
- data/lib/e11y/adapters/registry.rb +0 -141
- /data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +0 -0
data/lib/e11y/slo/tracker.rb
CHANGED
|
@@ -17,7 +17,7 @@ module E11y
|
|
|
17
17
|
#
|
|
18
18
|
# @example Enable SLO tracking
|
|
19
19
|
# E11y.configure do |config|
|
|
20
|
-
# config.
|
|
20
|
+
# config.slo_tracking_enabled = true
|
|
21
21
|
# end
|
|
22
22
|
#
|
|
23
23
|
# @example Track HTTP request
|
|
@@ -28,10 +28,31 @@ module E11y
|
|
|
28
28
|
# duration_ms: 42.5
|
|
29
29
|
# )
|
|
30
30
|
#
|
|
31
|
-
# @note C11 Resolution (
|
|
32
|
-
#
|
|
31
|
+
# @note C11 Resolution: Event-driven SLO (EventSlo middleware) applies stratified
|
|
32
|
+
# sampling correction via E11y::Sampling.stratified_tracker. HTTP/job SLO
|
|
33
|
+
# are tracked directly (no sampling) and need no correction.
|
|
33
34
|
module Tracker
|
|
35
|
+
# In-memory store for tracked request data (per endpoint).
|
|
36
|
+
# @api private Intended for test assertions only; not part of public API.
|
|
37
|
+
@_store = {}
|
|
38
|
+
|
|
34
39
|
class << self
|
|
40
|
+
# Return a snapshot of all tracked endpoints and their request counts.
|
|
41
|
+
# @api private Intended for test assertions only.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] Map of "controller#action" => { requests: Integer }
|
|
44
|
+
def status
|
|
45
|
+
@_store.dup
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Reset the in-memory store (useful for testing and per-request isolation).
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
# @api private
|
|
52
|
+
def reset!
|
|
53
|
+
@_store = {}
|
|
54
|
+
end
|
|
55
|
+
|
|
35
56
|
# Track HTTP request for SLO metrics.
|
|
36
57
|
#
|
|
37
58
|
# @param controller [String] Controller name
|
|
@@ -58,6 +79,12 @@ module E11y
|
|
|
58
79
|
labels.except(:status),
|
|
59
80
|
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
60
81
|
)
|
|
82
|
+
|
|
83
|
+
# Store in-memory for status reporting
|
|
84
|
+
key = "#{controller}##{action}"
|
|
85
|
+
@_store ||= {}
|
|
86
|
+
@_store[key] ||= { requests: 0 }
|
|
87
|
+
@_store[key][:requests] += 1
|
|
61
88
|
end
|
|
62
89
|
|
|
63
90
|
# Track background job for SLO metrics.
|
|
@@ -94,7 +121,7 @@ module E11y
|
|
|
94
121
|
#
|
|
95
122
|
# @return [Boolean] true if enabled
|
|
96
123
|
def enabled?
|
|
97
|
-
E11y.config.respond_to?(:
|
|
124
|
+
E11y.config.respond_to?(:slo_tracking_enabled) && E11y.config.slo_tracking_enabled
|
|
98
125
|
end
|
|
99
126
|
|
|
100
127
|
# Normalize HTTP status code to category (2xx, 3xx, 4xx, 5xx).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Testing
|
|
5
|
+
# RSpec matcher for asserting that an event was tracked during block execution.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# expect { OrdersController.create }.to have_tracked_event(Events::OrderCreated)
|
|
9
|
+
#
|
|
10
|
+
# @example With payload
|
|
11
|
+
# expect { action }.to have_tracked_event(Events::OrderPaid).with(order_id: 123)
|
|
12
|
+
#
|
|
13
|
+
# @example With count
|
|
14
|
+
# expect { action }.to have_tracked_event(Events::OrderPaid).once
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
16
|
+
class HaveTrackedEventMatcher
|
|
17
|
+
def initialize(event_class_or_pattern)
|
|
18
|
+
@event_class_or_pattern = event_class_or_pattern
|
|
19
|
+
@payload_matchers = {}
|
|
20
|
+
@severity_matcher = nil
|
|
21
|
+
@count_matcher = nil
|
|
22
|
+
@trace_id_matcher = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def with(payload_hash)
|
|
26
|
+
@payload_matchers = payload_hash
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def with_severity(severity)
|
|
31
|
+
@severity_matcher = severity
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def exactly(count)
|
|
36
|
+
@count_matcher = count
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def at_least(count)
|
|
41
|
+
@count_matcher = [:at_least, count]
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def at_most(count)
|
|
46
|
+
@count_matcher = [:at_most, count]
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def once
|
|
51
|
+
exactly(1)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def twice
|
|
55
|
+
exactly(2)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def with_trace_id(trace_id)
|
|
59
|
+
@trace_id_matcher = trace_id
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def matches?(actual = nil)
|
|
64
|
+
actual.call if actual.respond_to?(:call) # Execute block before checking events
|
|
65
|
+
@events = find_matching_events
|
|
66
|
+
return false if @events.empty?
|
|
67
|
+
return false unless count_matches?
|
|
68
|
+
return false unless payload_matches?
|
|
69
|
+
return false unless severity_matches?
|
|
70
|
+
return false unless trace_id_matches?
|
|
71
|
+
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def failure_message
|
|
76
|
+
if @events.empty?
|
|
77
|
+
no_events_message
|
|
78
|
+
elsif !count_matches?
|
|
79
|
+
count_mismatch_message
|
|
80
|
+
elsif !payload_matches?
|
|
81
|
+
payload_mismatch_message
|
|
82
|
+
elsif !severity_matches?
|
|
83
|
+
severity_mismatch_message
|
|
84
|
+
else
|
|
85
|
+
trace_id_mismatch_message
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def failure_message_when_negated
|
|
90
|
+
"expected not to have tracked #{event_name}, but it was tracked"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def supports_block_expectations?
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def find_matching_events
|
|
100
|
+
adapter = E11y.test_adapter
|
|
101
|
+
return [] unless adapter
|
|
102
|
+
|
|
103
|
+
adapter.find_events(@event_class_or_pattern)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def event_name
|
|
107
|
+
case @event_class_or_pattern
|
|
108
|
+
when Class
|
|
109
|
+
@event_class_or_pattern.name
|
|
110
|
+
else
|
|
111
|
+
@event_class_or_pattern.to_s
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def count_matches?
|
|
116
|
+
return true unless @count_matcher
|
|
117
|
+
|
|
118
|
+
case @count_matcher
|
|
119
|
+
when Integer
|
|
120
|
+
@events.size == @count_matcher
|
|
121
|
+
when Array
|
|
122
|
+
operator, expected = @count_matcher
|
|
123
|
+
case operator
|
|
124
|
+
when :at_least then @events.size >= expected
|
|
125
|
+
when :at_most then @events.size <= expected
|
|
126
|
+
else false
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def payload_matches?
|
|
132
|
+
return true if @payload_matchers.empty?
|
|
133
|
+
|
|
134
|
+
@events.any? do |event|
|
|
135
|
+
payload = event[:payload] || {}
|
|
136
|
+
@payload_matchers.all? do |key, expected_value|
|
|
137
|
+
actual_value = payload[key.to_s] || payload[key.to_sym]
|
|
138
|
+
actual_value == expected_value
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def severity_matches?
|
|
144
|
+
return true unless @severity_matcher
|
|
145
|
+
|
|
146
|
+
@events.any? { |event| event[:severity].to_s == @severity_matcher.to_s }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def trace_id_matches?
|
|
150
|
+
return true unless @trace_id_matcher
|
|
151
|
+
|
|
152
|
+
@events.any? { |event| event[:trace_id] == @trace_id_matcher }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def no_events_message
|
|
156
|
+
adapter = E11y.test_adapter
|
|
157
|
+
if !adapter || adapter.events.empty?
|
|
158
|
+
"expected to have tracked #{event_name}, but no events were tracked at all"
|
|
159
|
+
else
|
|
160
|
+
tracked = adapter.events.map { |e| e[:event_name] }.uniq.join(", ")
|
|
161
|
+
"expected to have tracked #{event_name}, but only tracked: #{tracked}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def count_mismatch_message
|
|
166
|
+
expected = case @count_matcher
|
|
167
|
+
when Integer then "exactly #{@count_matcher}"
|
|
168
|
+
when Array then "#{@count_matcher[0].to_s.tr('_', ' ')} #{@count_matcher[1]}"
|
|
169
|
+
end
|
|
170
|
+
"expected to track #{event_name} #{expected} times, but tracked #{@events.size} times"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def payload_mismatch_message
|
|
174
|
+
"expected #{event_name} with payload #{@payload_matchers.inspect}, " \
|
|
175
|
+
"but got:\n#{@events.map { |e| " #{e[:payload].inspect}" }.join("\n")}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def severity_mismatch_message
|
|
179
|
+
severities = @events.map { |e| e[:severity] }.uniq.join(", ")
|
|
180
|
+
"expected #{event_name} with severity :#{@severity_matcher}, but got: #{severities}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def trace_id_mismatch_message
|
|
184
|
+
trace_ids = @events.map { |e| e[:trace_id] }.uniq.join(", ")
|
|
185
|
+
"expected #{event_name} with trace_id #{@trace_id_matcher}, but got: #{trace_ids}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
# rubocop:enable Metrics/ClassLength
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Testing
|
|
5
|
+
# RSpec matchers for event tracking assertions (ADR-011 F-002)
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# expect { OrdersController.create }.to have_tracked_event(Events::OrderCreated)
|
|
9
|
+
# expect { action }.to have_tracked_event(Events::OrderPaid).with(order_id: 123)
|
|
10
|
+
# expect { action }.to have_tracked_event(Events::OrderPaid).once
|
|
11
|
+
module RSpecMatchers
|
|
12
|
+
# rubocop:disable Naming/PredicatePrefix -- RSpec matcher convention: have_tracked_event
|
|
13
|
+
def have_tracked_event(event_class_or_pattern)
|
|
14
|
+
HaveTrackedEventMatcher.new(event_class_or_pattern)
|
|
15
|
+
end
|
|
16
|
+
# rubocop:enable Naming/PredicatePrefix
|
|
17
|
+
|
|
18
|
+
alias track_event have_tracked_event
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Testing
|
|
8
|
+
# Snapshot matcher for event comparison (ADR-011 F-006).
|
|
9
|
+
#
|
|
10
|
+
# Compares event hashes against YAML snapshots, normalizing volatile fields
|
|
11
|
+
# (timestamp, trace_id, span_id). First run creates the snapshot; subsequent
|
|
12
|
+
# runs compare against it. Use UPDATE_SNAPSHOTS=1 to update snapshots.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# event = E11y.test_adapter.find_event(Events::OrderCreated)
|
|
16
|
+
# expect(event).to match_snapshot("order_created_event")
|
|
17
|
+
class SnapshotMatcher
|
|
18
|
+
SNAPSHOTS_DIR = "spec/snapshots/events"
|
|
19
|
+
VOLATILE_KEYS = %i[timestamp trace_id span_id retention_until routed_at].freeze
|
|
20
|
+
|
|
21
|
+
def initialize(snapshot_name)
|
|
22
|
+
@snapshot_name = snapshot_name
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def matches?(actual)
|
|
26
|
+
@actual = actual
|
|
27
|
+
@normalized = normalize_event(actual)
|
|
28
|
+
@snapshot_path = File.join(SNAPSHOTS_DIR, "#{@snapshot_name}.yml")
|
|
29
|
+
|
|
30
|
+
if update_snapshots? || !File.exist?(@snapshot_path)
|
|
31
|
+
write_snapshot(@normalized)
|
|
32
|
+
true
|
|
33
|
+
else
|
|
34
|
+
@expected = YAML.load_file(@snapshot_path)
|
|
35
|
+
@normalized == @expected
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def failure_message
|
|
40
|
+
if @expected
|
|
41
|
+
"expected event to match snapshot #{@snapshot_name}, but it differed:\n" \
|
|
42
|
+
"Expected:\n#{@expected.to_yaml}\n" \
|
|
43
|
+
"Actual (normalized):\n#{@normalized.to_yaml}"
|
|
44
|
+
else
|
|
45
|
+
"snapshot #{@snapshot_name} not found at #{@snapshot_path}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def failure_message_when_negated
|
|
50
|
+
"expected event not to match snapshot #{@snapshot_name}, but it did"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def normalize_event(event)
|
|
56
|
+
return {} if event.nil?
|
|
57
|
+
|
|
58
|
+
event = event.dup
|
|
59
|
+
VOLATILE_KEYS.each { |k| event.delete(k) }
|
|
60
|
+
event.delete(:context) # context contains trace_id, span_id, etc.
|
|
61
|
+
event.delete(:routing)
|
|
62
|
+
deep_stringify(event)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def deep_stringify(obj)
|
|
66
|
+
case obj
|
|
67
|
+
when Hash
|
|
68
|
+
obj.transform_values { |v| deep_stringify(v) }.transform_keys(&:to_s)
|
|
69
|
+
when Array
|
|
70
|
+
obj.map { |v| deep_stringify(v) }
|
|
71
|
+
else
|
|
72
|
+
obj
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def update_snapshots?
|
|
77
|
+
%w[1 true].include?(ENV.fetch("UPDATE_SNAPSHOTS", nil))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def write_snapshot(data)
|
|
81
|
+
FileUtils.mkdir_p(File.dirname(@snapshot_path))
|
|
82
|
+
File.write(@snapshot_path, data.to_yaml)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module TraceContext
|
|
5
|
+
# Trace entry sampler (ADR-005 §7).
|
|
6
|
+
# Decides if trace should be sampled; respects parent decision when configured.
|
|
7
|
+
class Sampler
|
|
8
|
+
class << self
|
|
9
|
+
def should_sample?(context = {})
|
|
10
|
+
cfg = E11y.config
|
|
11
|
+
respect = cfg&.tracing_respect_parent_sampling != false
|
|
12
|
+
|
|
13
|
+
return context[:sampled] if respect && context.key?(:sampled)
|
|
14
|
+
|
|
15
|
+
rate = determine_sample_rate(context, cfg)
|
|
16
|
+
rand < rate
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def determine_sample_rate(context, cfg)
|
|
22
|
+
return 1.0 if context[:error]
|
|
23
|
+
return 1.0 if cfg&.tracing_always_sample_if&.call(context)
|
|
24
|
+
|
|
25
|
+
if context[:event_name] && cfg&.tracing_per_event_sample_rates
|
|
26
|
+
rate = cfg.tracing_per_event_sample_rates[context[:event_name].to_s]
|
|
27
|
+
return rate if rate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
(cfg&.tracing_default_sample_rate || 0.1).to_f
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is only loaded explicitly via E11y::Tracing.install_faraday_middleware!
|
|
4
|
+
# (which calls require "faraday" first) and is NOT autoloaded by Zeitwerk on startup.
|
|
5
|
+
# Faraday is an optional dependency — see e11y.gemspec.
|
|
6
|
+
|
|
7
|
+
module E11y
|
|
8
|
+
module Tracing
|
|
9
|
+
# Faraday middleware that injects W3C traceparent header into outgoing requests.
|
|
10
|
+
#
|
|
11
|
+
# Register once via E11y::Tracing.install_faraday_middleware!, then use per connection:
|
|
12
|
+
#
|
|
13
|
+
# conn = Faraday.new(url: "https://api.example.com") do |f|
|
|
14
|
+
# f.request :e11y_tracing
|
|
15
|
+
# f.adapter Faraday.default_adapter
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @see E11y::Tracing.install_faraday_middleware!
|
|
19
|
+
# @see E11y::Tracing::Propagator
|
|
20
|
+
class FaradayMiddleware < ::Faraday::Middleware
|
|
21
|
+
# Inject traceparent into outgoing request headers and pass to next middleware.
|
|
22
|
+
#
|
|
23
|
+
# @param env [Faraday::Env] Faraday request environment
|
|
24
|
+
# @return [Faraday::Response]
|
|
25
|
+
def call(env)
|
|
26
|
+
Propagator.inject(env.request_headers)
|
|
27
|
+
@app.call(env)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Tracing
|
|
5
|
+
# Monkey-patch for Net::HTTP that injects W3C traceparent into every request.
|
|
6
|
+
#
|
|
7
|
+
# Applied via +prepend+ so it wraps the original +#request+ method:
|
|
8
|
+
#
|
|
9
|
+
# E11y::Tracing.patch_net_http!
|
|
10
|
+
# # From this point all Net::HTTP requests carry the traceparent header.
|
|
11
|
+
#
|
|
12
|
+
# The patch is idempotent — prepending twice is prevented by checking
|
|
13
|
+
# +Net::HTTP.ancestors+.
|
|
14
|
+
#
|
|
15
|
+
# @see E11y::Tracing.patch_net_http!
|
|
16
|
+
# @see E11y::Tracing::Propagator
|
|
17
|
+
module NetHTTPPatch
|
|
18
|
+
# Inject traceparent header then delegate to the original Net::HTTP#request.
|
|
19
|
+
#
|
|
20
|
+
# Skips injection if traceparent is already set on the request object
|
|
21
|
+
# (e.g., caller set it manually).
|
|
22
|
+
#
|
|
23
|
+
# @param req [Net::HTTPRequest] Outgoing HTTP request object
|
|
24
|
+
# @param body [String, nil] Optional body
|
|
25
|
+
# @return [Net::HTTPResponse]
|
|
26
|
+
def request(req, body = nil, &)
|
|
27
|
+
header_value = Propagator.build_traceparent
|
|
28
|
+
req[Propagator::TRACEPARENT_HEADER] = header_value if header_value && !req[Propagator::TRACEPARENT_HEADER]
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module E11y
|
|
6
|
+
module Tracing
|
|
7
|
+
# W3C Trace Context propagator.
|
|
8
|
+
#
|
|
9
|
+
# Builds, injects, and parses W3C traceparent headers.
|
|
10
|
+
# Format: {version}-{trace-id}-{parent-id}-{flags}
|
|
11
|
+
# Example: 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01
|
|
12
|
+
#
|
|
13
|
+
# @see https://www.w3.org/TR/trace-context/
|
|
14
|
+
# @see UC-009 Multi-Service Tracing
|
|
15
|
+
class Propagator
|
|
16
|
+
TRACEPARENT_VERSION = "00"
|
|
17
|
+
SAMPLED_FLAG = "01"
|
|
18
|
+
TRACEPARENT_HEADER = "traceparent"
|
|
19
|
+
TRACESTATE_HEADER = "tracestate"
|
|
20
|
+
|
|
21
|
+
# Build a W3C traceparent header value from the current trace context.
|
|
22
|
+
#
|
|
23
|
+
# Falls back to E11y::Current if explicit ids are not provided.
|
|
24
|
+
# Generates a random span_id if none is set.
|
|
25
|
+
# Returns nil when no trace_id is available.
|
|
26
|
+
#
|
|
27
|
+
# @param trace_id [String, nil] Override trace_id (optional)
|
|
28
|
+
# @param span_id [String, nil] Override span_id (optional)
|
|
29
|
+
# @return [String, nil] e.g. "00-abc...32hex-def...16hex-01", or nil
|
|
30
|
+
def self.build_traceparent(trace_id: nil, span_id: nil)
|
|
31
|
+
t_id = trace_id || E11y::Current.trace_id
|
|
32
|
+
return nil if t_id.nil? || t_id.empty?
|
|
33
|
+
|
|
34
|
+
s_id = span_id || E11y::Current.span_id
|
|
35
|
+
s_id = SecureRandom.hex(8) if s_id.nil? || s_id.empty?
|
|
36
|
+
|
|
37
|
+
sampled = E11y::Current.respond_to?(:sampled) ? E11y::Current.sampled : true
|
|
38
|
+
flags = sampled == false ? "00" : SAMPLED_FLAG
|
|
39
|
+
"#{TRACEPARENT_VERSION}-#{t_id}-#{s_id}-#{flags}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Inject W3C trace context headers into a plain Hash of headers.
|
|
43
|
+
#
|
|
44
|
+
# Mutates +headers+ in place and returns it.
|
|
45
|
+
# Does NOT override an existing traceparent entry.
|
|
46
|
+
# Adds tracestate when E11y::Current.baggage is present (F-014).
|
|
47
|
+
#
|
|
48
|
+
# @param headers [Hash] Headers hash to mutate
|
|
49
|
+
# @param trace_id [String, nil] Override trace_id (optional)
|
|
50
|
+
# @param span_id [String, nil] Override span_id (optional)
|
|
51
|
+
# @return [Hash] The (possibly mutated) headers hash
|
|
52
|
+
def self.inject(headers, trace_id: nil, span_id: nil)
|
|
53
|
+
unless headers[TRACEPARENT_HEADER]
|
|
54
|
+
header_value = build_traceparent(trace_id: trace_id, span_id: span_id)
|
|
55
|
+
headers[TRACEPARENT_HEADER] = header_value if header_value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if !headers[TRACESTATE_HEADER] && E11y::Current.respond_to?(:baggage) && E11y::Current.baggage&.any?
|
|
59
|
+
filtered = filter_baggage_for_propagation(E11y::Current.baggage)
|
|
60
|
+
headers[TRACESTATE_HEADER] = build_tracestate(filtered) if filtered.any?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
headers
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Parse W3C tracestate header to Hash (key=value pairs).
|
|
67
|
+
# @param tracestate [String, nil] Raw header value
|
|
68
|
+
# @return [Hash] String keys and values (empty hash if invalid)
|
|
69
|
+
def self.parse_tracestate(tracestate)
|
|
70
|
+
return {} unless tracestate.is_a?(String)
|
|
71
|
+
|
|
72
|
+
tracestate.split(",").each_with_object({}) do |entry, hash|
|
|
73
|
+
key, value = entry.split("=", 2)
|
|
74
|
+
hash[key.strip] = value.to_s.strip if key && !key.strip.empty?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Build W3C tracestate header from baggage Hash.
|
|
79
|
+
# @param baggage_hash [Hash] String keys and values
|
|
80
|
+
# @return [String] e.g. "key1=value1,key2=value2"
|
|
81
|
+
def self.build_tracestate(baggage_hash)
|
|
82
|
+
return "" unless baggage_hash.is_a?(Hash) && baggage_hash.any?
|
|
83
|
+
|
|
84
|
+
baggage_hash.map { |k, v| "#{k}=#{v}" }.join(",")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Filter baggage to allowed keys only (ADR-006 §5.5, PII protection).
|
|
88
|
+
def self.filter_baggage_for_propagation(baggage_hash)
|
|
89
|
+
cfg = E11y.config
|
|
90
|
+
return baggage_hash if cfg.nil?
|
|
91
|
+
|
|
92
|
+
cfg.filter_baggage_for_propagation(baggage_hash)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Parse a W3C traceparent header string.
|
|
96
|
+
#
|
|
97
|
+
# @param traceparent [String, nil] Raw header value
|
|
98
|
+
# @return [Hash, nil] +{ trace_id:, parent_span_id:, sampled: }+ or nil if invalid
|
|
99
|
+
def self.parse(traceparent)
|
|
100
|
+
return nil unless traceparent.is_a?(String)
|
|
101
|
+
|
|
102
|
+
parts = traceparent.split("-")
|
|
103
|
+
return nil unless parts.size == 4
|
|
104
|
+
|
|
105
|
+
_version, trace_id, parent_span_id, flags = parts
|
|
106
|
+
return nil if trace_id.nil? || trace_id.empty?
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
trace_id: trace_id,
|
|
110
|
+
parent_span_id: parent_span_id,
|
|
111
|
+
sampled: flags == SAMPLED_FLAG
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/e11y/tracing.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
# Outgoing HTTP trace context propagation (UC-009).
|
|
5
|
+
#
|
|
6
|
+
# Provides W3C Trace Context injection into outgoing HTTP requests
|
|
7
|
+
# via Faraday middleware and Net::HTTP monkey-patch.
|
|
8
|
+
#
|
|
9
|
+
# @example Enable Net::HTTP tracing
|
|
10
|
+
# E11y::Tracing.patch_net_http!
|
|
11
|
+
#
|
|
12
|
+
# @example Enable Faraday tracing (register middleware, then use in connection)
|
|
13
|
+
# E11y::Tracing.install_faraday_middleware!
|
|
14
|
+
# conn = Faraday.new { |f| f.request :e11y_tracing }
|
|
15
|
+
#
|
|
16
|
+
# @see UC-009 Multi-Service Tracing
|
|
17
|
+
# @see https://www.w3.org/TR/trace-context/
|
|
18
|
+
module Tracing
|
|
19
|
+
# Install Net::HTTP tracing patch (idempotent).
|
|
20
|
+
#
|
|
21
|
+
# Prepends E11y::Tracing::NetHTTPPatch into Net::HTTP so that every
|
|
22
|
+
# outgoing request automatically carries a W3C traceparent header.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def self.patch_net_http!
|
|
26
|
+
require "net/http"
|
|
27
|
+
require "e11y/tracing/net_http_patch"
|
|
28
|
+
return if ::Net::HTTP <= E11y::Tracing::NetHTTPPatch
|
|
29
|
+
|
|
30
|
+
::Net::HTTP.prepend(E11y::Tracing::NetHTTPPatch)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Register the Faraday middleware so it can be referenced by name (idempotent).
|
|
34
|
+
#
|
|
35
|
+
# After calling this, add +f.request :e11y_tracing+ to any Faraday connection
|
|
36
|
+
# that should propagate trace context.
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
def self.install_faraday_middleware!
|
|
40
|
+
require "faraday"
|
|
41
|
+
require "e11y/tracing/faraday_middleware"
|
|
42
|
+
return if ::Faraday::Request.registered_middleware.key?(:e11y_tracing)
|
|
43
|
+
|
|
44
|
+
::Faraday::Request.register_middleware(e11y_tracing: E11y::Tracing::FaradayMiddleware)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/e11y/version.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Versioning
|
|
5
|
+
# Extracts version number and base name from event class names (ADR-012 §3.2).
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# VersionExtractor.extract_version("Events::OrderPaidV2") # => 2
|
|
9
|
+
# VersionExtractor.extract_version("Events::OrderPaid") # => 1
|
|
10
|
+
# VersionExtractor.extract_base_name("Events::OrderPaidV2") # => "Events::OrderPaid"
|
|
11
|
+
class VersionExtractor
|
|
12
|
+
VERSION_REGEX = /V(\d+)$/
|
|
13
|
+
|
|
14
|
+
# @param class_name [String] Event class name (e.g. "Events::OrderPaidV2")
|
|
15
|
+
# @return [Integer] Version number (1 if no suffix)
|
|
16
|
+
def self.extract_version(class_name)
|
|
17
|
+
return 1 unless class_name
|
|
18
|
+
|
|
19
|
+
match = class_name.to_s.match(VERSION_REGEX)
|
|
20
|
+
match ? match[1].to_i : 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param class_name [String] Event class name
|
|
24
|
+
# @return [String] Base name without version suffix (e.g. "Events::OrderPaid")
|
|
25
|
+
def self.extract_base_name(class_name)
|
|
26
|
+
return class_name unless class_name
|
|
27
|
+
|
|
28
|
+
class_name.to_s.sub(VERSION_REGEX, "")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|