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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
106
|
+
# Get current trace ID from configured source (ADR-007 §8).
|
|
107
107
|
#
|
|
108
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
VERSION_REGEX =
|
|
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
|
-
|
|
66
|
-
|
|
68
|
+
klass = event_data[:event_class]
|
|
69
|
+
class_name = klass&.name
|
|
67
70
|
|
|
68
|
-
|
|
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
|
-
#
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
data/lib/e11y/pii/patterns.rb
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|