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
|
@@ -44,9 +44,7 @@ module E11y
|
|
|
44
44
|
def self.signing_key
|
|
45
45
|
@signing_key ||= ENV.fetch("E11Y_AUDIT_SIGNING_KEY") do
|
|
46
46
|
# Development fallback (NOT for production!)
|
|
47
|
-
if defined?(::Rails) && ::Rails.env.production?
|
|
48
|
-
raise E11y::Error, "E11Y_AUDIT_SIGNING_KEY must be set in production"
|
|
49
|
-
end
|
|
47
|
+
raise E11y::Error, "E11Y_AUDIT_SIGNING_KEY must be set in production" if defined?(::Rails) && ::Rails.env.production?
|
|
50
48
|
|
|
51
49
|
"development_key_#{SecureRandom.hex(32)}"
|
|
52
50
|
end
|
|
@@ -73,20 +71,59 @@ module E11y
|
|
|
73
71
|
|
|
74
72
|
# Verify signature (for testing/validation)
|
|
75
73
|
#
|
|
74
|
+
# Uses the stored audit_canonical to recompute the expected HMAC and compares
|
|
75
|
+
# against audit_signature. Detects tampering with the canonical representation
|
|
76
|
+
# (e.g., if someone modifies the stored canonical in the audit log).
|
|
77
|
+
#
|
|
76
78
|
# @param event_data [Hash] Event data with signature
|
|
77
79
|
# @return [Boolean] true if signature is valid
|
|
78
80
|
# rubocop:disable Naming/PredicateMethod
|
|
79
81
|
def self.verify_signature(event_data)
|
|
80
82
|
expected_signature = event_data[:audit_signature]
|
|
81
|
-
|
|
83
|
+
return false unless expected_signature
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
# Recompute canonical from CURRENT payload (detects payload tampering)
|
|
86
|
+
recomputed = canonical_representation(event_data)
|
|
87
|
+
# Verify stored canonical matches recomputed (detects canonical tampering)
|
|
88
|
+
return false if event_data[:audit_canonical] && event_data[:audit_canonical] != recomputed
|
|
84
89
|
|
|
85
|
-
actual_signature = OpenSSL::HMAC.hexdigest("SHA256", signing_key,
|
|
90
|
+
actual_signature = OpenSSL::HMAC.hexdigest("SHA256", signing_key, recomputed)
|
|
86
91
|
actual_signature == expected_signature
|
|
87
92
|
end
|
|
88
93
|
# rubocop:enable Naming/PredicateMethod
|
|
89
94
|
|
|
95
|
+
# Create canonical representation for signing (class method for verification)
|
|
96
|
+
#
|
|
97
|
+
# @param event_data [Hash] Event data
|
|
98
|
+
# @return [String] Canonical JSON string
|
|
99
|
+
def self.canonical_representation(event_data)
|
|
100
|
+
# Extract fields that should be signed
|
|
101
|
+
signable_data = {
|
|
102
|
+
event_name: event_data[:event_name],
|
|
103
|
+
payload: event_data[:payload],
|
|
104
|
+
timestamp: event_data[:timestamp],
|
|
105
|
+
version: event_data[:version]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Convert to sorted JSON (deterministic)
|
|
109
|
+
JSON.generate(sort_hash(signable_data))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Sort hash recursively for deterministic JSON (class method)
|
|
113
|
+
#
|
|
114
|
+
# @param obj [Object] Object to sort
|
|
115
|
+
# @return [Object] Sorted object
|
|
116
|
+
def self.sort_hash(obj)
|
|
117
|
+
case obj
|
|
118
|
+
when Hash
|
|
119
|
+
obj.keys.sort.to_h { |k| [k, sort_hash(obj[k])] }
|
|
120
|
+
when Array
|
|
121
|
+
obj.map { |v| sort_hash(v) }
|
|
122
|
+
else
|
|
123
|
+
obj
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
90
127
|
private
|
|
91
128
|
|
|
92
129
|
# Check if event is marked as audit event
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# BaggageProtection middleware — blocks PII from OpenTelemetry Baggage (ADR-006 §5.5, C08).
|
|
6
|
+
#
|
|
7
|
+
# When enabled, prepends an interceptor to OpenTelemetry::Baggage that blocks
|
|
8
|
+
# set_value calls for keys not in the allowlist. Prevents PII from propagating
|
|
9
|
+
# via W3C Baggage headers to downstream services.
|
|
10
|
+
#
|
|
11
|
+
# @example Configuration
|
|
12
|
+
# E11y.configure do |config|
|
|
13
|
+
# config.security_baggage_protection_enabled = true
|
|
14
|
+
# config.security_baggage_protection_allowed_keys = %w[trace_id span_id request_id]
|
|
15
|
+
# config.security_baggage_protection_block_mode = :warn # :silent, :warn, :raise
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @see ADR-006 §5.5 OpenTelemetry Baggage PII Protection
|
|
19
|
+
# @see CONFLICT-ANALYSIS.md C08
|
|
20
|
+
class BaggageProtection < Base
|
|
21
|
+
middleware_zone :security
|
|
22
|
+
|
|
23
|
+
def initialize(app)
|
|
24
|
+
super
|
|
25
|
+
@protected = false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call(event_data)
|
|
29
|
+
protect_baggage! if should_protect?
|
|
30
|
+
@app.call(event_data)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def should_protect?
|
|
36
|
+
return false unless defined?(OpenTelemetry::Baggage)
|
|
37
|
+
return false unless E11y.config&.security_baggage_protection_enabled
|
|
38
|
+
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def protect_baggage!
|
|
43
|
+
return if @protected
|
|
44
|
+
|
|
45
|
+
@protected = true
|
|
46
|
+
cfg = E11y.config
|
|
47
|
+
allowed_keys = (cfg&.security_baggage_protection_allowed_keys || E11y::BAGGAGE_PROTECTION_DEFAULT_ALLOWED_KEYS).map(&:to_s)
|
|
48
|
+
block_mode = cfg&.security_baggage_protection_block_mode || :silent
|
|
49
|
+
logger = E11y.logger
|
|
50
|
+
|
|
51
|
+
interceptor = build_interceptor(allowed_keys, block_mode, logger)
|
|
52
|
+
# Baggage uses extend self, so prepend to the module (instance methods become singleton)
|
|
53
|
+
OpenTelemetry::Baggage.prepend(interceptor)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_interceptor(allowed_keys, block_mode, logger)
|
|
57
|
+
Module.new do
|
|
58
|
+
define_method(:set_value) do |key, value, metadata: nil, context: nil|
|
|
59
|
+
ctx = context || (defined?(OpenTelemetry::Context) && OpenTelemetry::Context.current)
|
|
60
|
+
unless allowed_keys.include?(key.to_s)
|
|
61
|
+
message = "[E11y] Blocked PII from OpenTelemetry baggage: key=#{key.inspect}"
|
|
62
|
+
case block_mode
|
|
63
|
+
when :silent then logger&.debug(message)
|
|
64
|
+
when :warn then logger&.warn(message)
|
|
65
|
+
when :raise then raise E11y::BaggagePiiError, "#{message}. Only allowed keys: #{allowed_keys.join(', ')}"
|
|
66
|
+
end
|
|
67
|
+
return ctx
|
|
68
|
+
end
|
|
69
|
+
super(key, value, metadata: metadata, context: ctx)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# Sets Thread.current[:e11y_source] = "web" during a web request.
|
|
6
|
+
# Cleared after the request completes (even on exception).
|
|
7
|
+
#
|
|
8
|
+
# Also propagates trace_id to Rack env for the Browser Overlay:
|
|
9
|
+
# env["e11y.trace_id"] is set from Thread.current[:e11y_trace_id].
|
|
10
|
+
class DevLogSource
|
|
11
|
+
def initialize(app)
|
|
12
|
+
@app = app
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(env)
|
|
16
|
+
Thread.current[:e11y_source] = "web"
|
|
17
|
+
env["e11y.trace_id"] ||= Thread.current[:e11y_trace_id]
|
|
18
|
+
@app.call(env)
|
|
19
|
+
ensure
|
|
20
|
+
Thread.current[:e11y_source] = nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -69,23 +69,30 @@ module E11y
|
|
|
69
69
|
# Skip if SLO not enabled for this event
|
|
70
70
|
# Support explicit event_class (for testing) or resolve from event_name
|
|
71
71
|
event_class = event_data[:event_class] || resolve_event_class(event_data)
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
unless event_class.respond_to?(:slo_config) && event_class.slo_config&.enabled?
|
|
73
|
+
# Pass to next middleware even if SLO not enabled
|
|
74
|
+
return @app&.call(event_data) || event_data
|
|
75
|
+
end
|
|
74
76
|
|
|
75
77
|
# Compute slo_status from payload
|
|
76
78
|
slo_status = compute_slo_status(event_class, event_data[:payload])
|
|
77
|
-
|
|
79
|
+
unless slo_status
|
|
80
|
+
# Pass to next middleware even if slo_status is nil
|
|
81
|
+
return @app&.call(event_data) || event_data
|
|
82
|
+
end
|
|
78
83
|
|
|
79
|
-
# Emit SLO metric
|
|
80
|
-
emit_slo_metric(event_class, slo_status, event_data[:payload])
|
|
84
|
+
# Emit SLO metric (with sampling correction when stratified sampling enabled)
|
|
85
|
+
emit_slo_metric(event_class, slo_status, event_data[:payload], event_data)
|
|
81
86
|
|
|
82
|
-
|
|
87
|
+
# Pass to next middleware (Routing writes to adapters)
|
|
88
|
+
@app&.call(event_data) || event_data
|
|
83
89
|
rescue StandardError => e
|
|
84
90
|
# Never fail event tracking due to SLO processing
|
|
85
91
|
E11y.logger.error(
|
|
86
92
|
"[E11y::Middleware::EventSlo] SLO processing failed for #{event_data[:event_name]}: #{e.message}"
|
|
87
93
|
)
|
|
88
|
-
|
|
94
|
+
# Still pass to next middleware even on error
|
|
95
|
+
@app&.call(event_data) || event_data
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
private
|
|
@@ -124,15 +131,22 @@ module E11y
|
|
|
124
131
|
end
|
|
125
132
|
|
|
126
133
|
# Emit SLO metric to Yabeda/Prometheus.
|
|
134
|
+
# C11: Applies stratified sampling correction when event was sampled.
|
|
127
135
|
#
|
|
128
136
|
# @param event_class [Class] Event class
|
|
129
137
|
# @param slo_status [String] 'success' or 'failure'
|
|
130
138
|
# @param payload [Hash] Event payload
|
|
139
|
+
# @param event_data [Hash] Full event data (for sample_rate)
|
|
131
140
|
# @return [void]
|
|
132
|
-
def emit_slo_metric(event_class, slo_status, payload)
|
|
141
|
+
def emit_slo_metric(event_class, slo_status, payload, _event_data = {})
|
|
133
142
|
labels = build_slo_labels(event_class, slo_status, payload)
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
# C11: Apply sampling correction for accurate SLO with stratified sampling
|
|
145
|
+
stratum = slo_status == "success" ? :success : :error
|
|
146
|
+
correction = E11y::Sampling.stratified_tracker.sampling_correction(stratum)
|
|
147
|
+
value = (correction * 100).round / 100.0 # Round to 2 decimals for Prometheus
|
|
148
|
+
|
|
149
|
+
E11y::Metrics.increment(:slo_event_result_total, labels, value: value)
|
|
136
150
|
rescue StandardError => e
|
|
137
151
|
E11y.logger.error(
|
|
138
152
|
"[E11y::Middleware::EventSlo] Failed to emit SLO metric for #{event_class.name}: #{e.message}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# OtelSpan middleware — creates OpenTelemetry spans from events (ADR-007 §6, F2).
|
|
6
|
+
#
|
|
7
|
+
# When config.opentelemetry_span_creation_patterns is set, creates OTel spans
|
|
8
|
+
# for matching events. Errors/fatal always create spans.
|
|
9
|
+
#
|
|
10
|
+
# @see E11y::OpenTelemetry::SpanCreator
|
|
11
|
+
# @see ADR-007 §6 Traces Signal Export
|
|
12
|
+
class OtelSpan < Base
|
|
13
|
+
middleware_zone :adapters
|
|
14
|
+
|
|
15
|
+
def call(event_data)
|
|
16
|
+
if defined?(::OpenTelemetry::Trace) && defined?(E11y::OpenTelemetry::SpanCreator)
|
|
17
|
+
E11y::OpenTelemetry::SpanCreator.create_span_from_event(event_data)
|
|
18
|
+
end
|
|
19
|
+
@app.call(event_data)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "active_support/parameter_filter"
|
|
4
|
+
|
|
3
5
|
module E11y
|
|
4
6
|
module Middleware
|
|
5
|
-
# PII Filter Middleware
|
|
7
|
+
# PII Filter Middleware
|
|
6
8
|
#
|
|
7
9
|
# Filters Personally Identifiable Information (PII) from event payloads
|
|
8
|
-
# before they reach adapters. Implements ADR-006
|
|
10
|
+
# before they reach adapters. Implements ADR-006 security model.
|
|
9
11
|
#
|
|
10
|
-
# **
|
|
11
|
-
# -
|
|
12
|
-
# -
|
|
13
|
-
# -
|
|
12
|
+
# **Filtering modes:**
|
|
13
|
+
# - :no_pii — Skip filtering (contains_pii false, 0ms overhead)
|
|
14
|
+
# - :rails_filters — Rails filter_parameters only (~0.05ms overhead)
|
|
15
|
+
# - :explicit_pii — Field strategies, optionally per-adapter via exclude_adapters (~0.2ms)
|
|
14
16
|
#
|
|
15
|
-
# @example Basic Usage (
|
|
17
|
+
# @example Basic Usage (:rails_filters - default)
|
|
16
18
|
# class Events::OrderCreated < E11y::Event::Base
|
|
17
19
|
# schema do
|
|
18
20
|
# required(:order_id).filled(:string)
|
|
@@ -20,12 +22,12 @@ module E11y
|
|
|
20
22
|
# end
|
|
21
23
|
# end
|
|
22
24
|
#
|
|
23
|
-
# @example
|
|
25
|
+
# @example :no_pii (skip filtering)
|
|
24
26
|
# class Events::HealthCheck < E11y::Event::Base
|
|
25
|
-
# contains_pii false
|
|
27
|
+
# contains_pii false
|
|
26
28
|
# end
|
|
27
29
|
#
|
|
28
|
-
# @example
|
|
30
|
+
# @example :explicit_pii (field strategies)
|
|
29
31
|
# class Events::UserRegistered < E11y::Event::Base
|
|
30
32
|
# contains_pii true
|
|
31
33
|
#
|
|
@@ -40,7 +42,6 @@ module E11y
|
|
|
40
42
|
# @see UC-007 PII Filtering
|
|
41
43
|
# @see E11y::PII::Patterns
|
|
42
44
|
# rubocop:disable Metrics/ClassLength
|
|
43
|
-
# PII filter is a cohesive security component with 3-tier filtering strategy
|
|
44
45
|
class PIIFilter < Base
|
|
45
46
|
middleware_zone :security
|
|
46
47
|
|
|
@@ -53,27 +54,24 @@ module E11y
|
|
|
53
54
|
@config = config
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
# Process event and filter PII based on
|
|
57
|
+
# Process event and filter PII based on filtering mode
|
|
57
58
|
#
|
|
58
59
|
# @param event_data [Hash] Event data with payload
|
|
59
60
|
# @return [Hash] Processed event data
|
|
60
61
|
# rubocop:disable Lint/DuplicateBranch
|
|
61
|
-
# Unknown tiers intentionally fallback to no filtering (same as tier1)
|
|
62
62
|
def call(event_data)
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
return @app.call(event_data) if event_data[:dlq_replayed]
|
|
64
|
+
|
|
65
|
+
mode = filtering_mode(event_data)
|
|
65
66
|
|
|
66
|
-
case
|
|
67
|
-
when :
|
|
68
|
-
# Tier 1: No PII - Skip filtering (0ms overhead)
|
|
67
|
+
case mode
|
|
68
|
+
when :no_pii
|
|
69
69
|
@app.call(event_data)
|
|
70
|
-
when :
|
|
71
|
-
# Tier 2: Rails filters only (~0.05ms overhead)
|
|
70
|
+
when :rails_filters
|
|
72
71
|
filtered_data = apply_rails_filters(event_data)
|
|
73
72
|
@app.call(filtered_data)
|
|
74
|
-
when :
|
|
75
|
-
|
|
76
|
-
filtered_data = apply_deep_filtering(event_data)
|
|
73
|
+
when :explicit_pii
|
|
74
|
+
filtered_data = apply_explicit_pii_filtering(event_data)
|
|
77
75
|
@app.call(filtered_data)
|
|
78
76
|
else
|
|
79
77
|
@app.call(event_data)
|
|
@@ -83,16 +81,11 @@ module E11y
|
|
|
83
81
|
|
|
84
82
|
private
|
|
85
83
|
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
# @param event_data [Hash] Event data
|
|
89
|
-
# @return [Symbol] :tier1, :tier2, or :tier3
|
|
90
|
-
def determine_tier(event_data)
|
|
84
|
+
def filtering_mode(event_data)
|
|
91
85
|
event_class = event_data[:event_class]
|
|
92
|
-
return :
|
|
86
|
+
return :rails_filters unless event_class.respond_to?(:pii_filtering_mode)
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
event_class.pii_tier
|
|
88
|
+
event_class.pii_filtering_mode
|
|
96
89
|
end
|
|
97
90
|
|
|
98
91
|
# Apply Rails filter_parameters (Tier 2)
|
|
@@ -109,52 +102,71 @@ module E11y
|
|
|
109
102
|
filtered_data
|
|
110
103
|
end
|
|
111
104
|
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
# @param event_data [Hash] Event data
|
|
115
|
-
# @return [Hash] Filtered event data
|
|
116
|
-
def apply_deep_filtering(event_data)
|
|
105
|
+
# :explicit_pii — field strategies, optionally payload_rewrites when exclude_adapters present.
|
|
106
|
+
def apply_explicit_pii_filtering(event_data)
|
|
117
107
|
event_class = event_data[:event_class]
|
|
118
108
|
return event_data unless event_class
|
|
119
109
|
|
|
120
|
-
# Clone to avoid modifying original
|
|
121
|
-
filtered_data = deep_dup(event_data)
|
|
122
|
-
|
|
123
|
-
# Get PII filtering config from event class
|
|
124
110
|
pii_config = event_class.pii_filtering_config if event_class.respond_to?(:pii_filtering_config)
|
|
125
|
-
return
|
|
111
|
+
return event_data unless pii_config
|
|
112
|
+
|
|
113
|
+
# 1. Base payload (most restrictive)
|
|
114
|
+
base_payload = apply_field_strategies(deep_dup(event_data[:payload]), pii_config, nil)
|
|
115
|
+
base_payload = apply_pattern_filtering(base_payload, pii_config, [])
|
|
126
116
|
|
|
127
|
-
|
|
128
|
-
filtered_data[:payload] =
|
|
129
|
-
filtered_data[:payload],
|
|
130
|
-
pii_config
|
|
131
|
-
)
|
|
117
|
+
filtered_data = deep_dup(event_data)
|
|
118
|
+
filtered_data[:payload] = base_payload
|
|
132
119
|
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)
|
|
120
|
+
# 2. payload_rewrites: per-adapter overrides for exclude_adapters fields only
|
|
121
|
+
has_exclude_adapters = pii_config[:fields]&.any? { |_, v| v[:exclude_adapters]&.any? }
|
|
122
|
+
filtered_data[:payload_rewrites] = build_payload_rewrites(event_data, pii_config) if has_exclude_adapters
|
|
137
123
|
|
|
138
124
|
filtered_data
|
|
139
125
|
end
|
|
140
126
|
|
|
127
|
+
# Build payload_rewrites: { adapter_name => { field => original_value } }
|
|
128
|
+
# Only fields with exclude_adapters.include?(adapter) get original value.
|
|
129
|
+
def build_payload_rewrites(event_data, pii_config)
|
|
130
|
+
adapters = AdapterResolver.resolve(event_data)
|
|
131
|
+
return {} unless adapters.any?
|
|
132
|
+
|
|
133
|
+
original_payload = event_data[:payload] || {}
|
|
134
|
+
rewrites = {}
|
|
135
|
+
|
|
136
|
+
adapters.each do |adapter_name|
|
|
137
|
+
adapter_rewrites = {}
|
|
138
|
+
pii_config[:fields]&.each do |field, opts|
|
|
139
|
+
next unless opts[:exclude_adapters]&.include?(adapter_name)
|
|
140
|
+
|
|
141
|
+
key = original_payload.key?(field) ? field : field.to_s
|
|
142
|
+
adapter_rewrites[key] = original_payload[key] if original_payload.key?(key)
|
|
143
|
+
end
|
|
144
|
+
rewrites[adapter_name] = adapter_rewrites if adapter_rewrites.any?
|
|
145
|
+
end
|
|
146
|
+
rewrites
|
|
147
|
+
end
|
|
148
|
+
|
|
141
149
|
# Apply field-level filtering strategies
|
|
142
150
|
#
|
|
143
151
|
# @param payload [Hash] Payload to filter
|
|
144
152
|
# @param config [Hash] PII configuration
|
|
153
|
+
# @param adapter_name [Symbol, nil] When set, use :skip for fields with exclude_adapters.include?(adapter_name)
|
|
145
154
|
# @return [Hash] Filtered payload
|
|
146
|
-
# rubocop:disable Metrics/
|
|
147
|
-
|
|
148
|
-
def apply_field_strategies(payload, config)
|
|
155
|
+
# rubocop:disable Metrics/MethodLength
|
|
156
|
+
def apply_field_strategies(payload, config, adapter_name = nil)
|
|
149
157
|
return payload unless config
|
|
150
158
|
|
|
151
159
|
filtered = {}
|
|
152
160
|
|
|
153
161
|
payload.each do |key, value|
|
|
154
|
-
|
|
162
|
+
normalized_key = key.is_a?(Symbol) ? key : key.to_sym
|
|
163
|
+
field_config = config.dig(:fields, normalized_key) || {}
|
|
164
|
+
strategy = field_config[:strategy] || :allow
|
|
165
|
+
|
|
166
|
+
# Per-adapter: use :skip for excluded adapters (e.g. audit gets original)
|
|
167
|
+
strategy = :allow if adapter_name && field_config[:exclude_adapters]&.include?(adapter_name)
|
|
155
168
|
|
|
156
169
|
# rubocop:disable Lint/DuplicateBranch
|
|
157
|
-
# Unknown strategies intentionally fallback to allow (same as :allow)
|
|
158
170
|
filtered[key] = case strategy
|
|
159
171
|
when :mask
|
|
160
172
|
"[FILTERED]"
|
|
@@ -164,7 +176,7 @@ module E11y
|
|
|
164
176
|
partial_mask(value)
|
|
165
177
|
when :redact
|
|
166
178
|
nil
|
|
167
|
-
when :allow
|
|
179
|
+
when :allow, :skip
|
|
168
180
|
value
|
|
169
181
|
else
|
|
170
182
|
value
|
|
@@ -174,34 +186,45 @@ module E11y
|
|
|
174
186
|
|
|
175
187
|
filtered
|
|
176
188
|
end
|
|
177
|
-
# rubocop:enable Metrics/
|
|
189
|
+
# rubocop:enable Metrics/MethodLength
|
|
178
190
|
|
|
179
191
|
# Apply pattern-based filtering to string values
|
|
180
|
-
|
|
181
|
-
# @param data [Object] Data to filter (recursively)
|
|
182
|
-
# @return [Object] Filtered data
|
|
183
|
-
def apply_pattern_filtering(data)
|
|
192
|
+
def apply_pattern_filtering(data, pii_config = nil, path = [])
|
|
184
193
|
case data
|
|
185
|
-
when Hash
|
|
186
|
-
|
|
187
|
-
when
|
|
188
|
-
|
|
189
|
-
when String
|
|
190
|
-
filter_string_patterns(data)
|
|
191
|
-
else
|
|
192
|
-
data
|
|
194
|
+
when Hash then apply_pattern_filtering_hash(data, pii_config, path)
|
|
195
|
+
when Array then data.map { |v| apply_pattern_filtering(v, pii_config, path) }
|
|
196
|
+
when String then filter_string_if_needed(data, path, pii_config)
|
|
197
|
+
else data
|
|
193
198
|
end
|
|
194
199
|
end
|
|
195
200
|
|
|
196
|
-
|
|
201
|
+
def apply_pattern_filtering_hash(data, pii_config, path)
|
|
202
|
+
data.each_with_object({}) do |(k, v), acc|
|
|
203
|
+
key_sym = k.is_a?(Symbol) ? k : k.to_sym
|
|
204
|
+
acc[k] = apply_pattern_filtering(v, pii_config, path + [key_sym])
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def filter_string_if_needed(str, path, pii_config)
|
|
209
|
+
path_under_allowed_key?(path, pii_config) ? str : filter_string_patterns(str)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Check if any ancestor key in path is explicitly allowed
|
|
213
|
+
def path_under_allowed_key?(path, pii_config)
|
|
214
|
+
return false unless pii_config && pii_config[:fields]
|
|
215
|
+
|
|
216
|
+
allowed_keys = pii_config[:fields].select { |_k, v| %i[allow skip].include?(v[:strategy]) }.keys
|
|
217
|
+
path.any? { |p| allowed_keys.include?(p) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Filter PII patterns in string (VALUE_PATTERNS only, not PASSWORD_FIELDS)
|
|
197
221
|
#
|
|
198
222
|
# @param str [String] String to filter
|
|
199
223
|
# @return [String] Filtered string
|
|
200
224
|
def filter_string_patterns(str)
|
|
201
225
|
result = str.dup
|
|
202
226
|
|
|
203
|
-
|
|
204
|
-
E11y::PII::Patterns::ALL.each do |pattern|
|
|
227
|
+
E11y::PII::Patterns::VALUE_PATTERNS.each do |pattern|
|
|
205
228
|
result = result.gsub(pattern, "[FILTERED]")
|
|
206
229
|
end
|
|
207
230
|
|
|
@@ -261,12 +284,18 @@ module E11y
|
|
|
261
284
|
# Get Rails parameter filter
|
|
262
285
|
#
|
|
263
286
|
# Uses Rails.application.config.filter_parameters for PII filtering.
|
|
287
|
+
# When Rails is not loaded (e.g. unit tests), uses empty filter (no-op).
|
|
264
288
|
#
|
|
265
289
|
# @return [ActiveSupport::ParameterFilter] Parameter filter
|
|
266
290
|
def parameter_filter
|
|
267
|
-
@parameter_filter
|
|
268
|
-
|
|
269
|
-
)
|
|
291
|
+
return @parameter_filter if defined?(@parameter_filter) && !@parameter_filter.nil?
|
|
292
|
+
|
|
293
|
+
filters = if defined?(Rails) && Rails.application
|
|
294
|
+
Rails.application.config.filter_parameters
|
|
295
|
+
else
|
|
296
|
+
[]
|
|
297
|
+
end
|
|
298
|
+
@parameter_filter = ActiveSupport::ParameterFilter.new(filters)
|
|
270
299
|
end
|
|
271
300
|
end
|
|
272
301
|
# rubocop:enable Metrics/ClassLength
|