e11y 0.2.0 → 1.1.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 +80 -1
- data/CLAUDE.md +168 -0
- data/CONTRIBUTING.md +640 -0
- data/README.md +165 -701
- data/RELEASE.md +41 -12
- data/Rakefile +249 -57
- 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 +79 -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} +36 -65
- data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
- data/docs/architecture/ADR-003-slo-observability.md +1402 -0
- 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} +182 -743
- 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} +44 -86
- data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +11 -11
- 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} +43 -59
- data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +58 -355
- 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/plans/2026-03-20-browser-overlay-svelte.md +281 -0
- 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 +33 -684
- 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 +30 -178
- data/docs/use_cases/UC-010-background-job-tracking.md +24 -91
- 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 +158 -0
- data/gems/e11y-devtools/config/routes.rb +15 -0
- data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
- data/gems/e11y-devtools/exe/e11y +34 -0
- data/gems/e11y-devtools/frontend/.gitignore +24 -0
- data/gems/e11y-devtools/frontend/README.md +51 -0
- data/gems/e11y-devtools/frontend/index.html +14 -0
- data/gems/e11y-devtools/frontend/package-lock.json +3707 -0
- data/gems/e11y-devtools/frontend/package.json +28 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/events/recent.json +4205 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/interactions.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0a2e04027cfa22d014bc22e8b27cd913/events.json +86 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0e1543af6a630fb3af6b52283154b3e0/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/1838b691faa49564f97db8592ff3978d/events.json +78 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/29f198f6588dacffb687777eb5f8f118/events.json +197 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/34bc3c9c0097de28a7a6f99b90a8e7bc/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/3ba6c20d068ab9cee00e51b180e66444/events.json +184 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/435bfd8f17b9009146a79812d7c3726d/events.json +144 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/4c7676e3fe668e99edb2b94d7d5678a9/events.json +222 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/6daf0d47974bedfc55d5de7004a3ea9f/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8a81ada42834d15f287bb40010043605/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8c0a98900edaae105469df8daedccf02/events.json +198 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8e4f645180f8a7d1dce426b07380466b/events.json +222 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/93db346fa5d44a032605a13b627f4b80/events.json +128 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/98ff6146faf7bd9be8bd03a8275817ba/events.json +223 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/9997ddd0247bc7e25f2ca7a5c415c93d/events.json +197 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/99e35f8ef3baedd798cc4fd085980ad9/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b4f3095c1909924cbc98889a86c83d6d/events.json +131 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b54b7fc32b7575a7110de809d11ccda0/events.json +128 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c0b48033fa06746bcc5886745e053cff/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c44649ac76701b4558927cd2305ab535/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/d601ae3320057580a39dbdac2edfdf4a/events.json +248 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e67e724bab422d2b52eeb49635e512e1/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e6c72765a28f158a8485b35fa63f73da/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/f541b87405c9a54819b18ebe529f6419/events.json +194 -0
- data/gems/e11y-devtools/frontend/scripts/generate_mocks.rb +397 -0
- data/gems/e11y-devtools/frontend/src/App.svelte +827 -0
- data/gems/e11y-devtools/frontend/src/components/Fab.svelte +19 -0
- data/gems/e11y-devtools/frontend/src/components/FilterBar.svelte +38 -0
- data/gems/e11y-devtools/frontend/src/components/FullscreenPanel.svelte +82 -0
- data/gems/e11y-devtools/frontend/src/components/InteractionsTimeline.svelte +264 -0
- data/gems/e11y-devtools/frontend/src/components/RecentHistogram.svelte +354 -0
- data/gems/e11y-devtools/frontend/src/lib/api.ts +37 -0
- data/gems/e11y-devtools/frontend/src/lib/eventIdentity.ts +12 -0
- data/gems/e11y-devtools/frontend/src/lib/format.ts +37 -0
- data/gems/e11y-devtools/frontend/src/lib/listFilter.ts +43 -0
- data/gems/e11y-devtools/frontend/src/lib/recentVolume.ts +80 -0
- data/gems/e11y-devtools/frontend/src/lib/router.ts +12 -0
- data/gems/e11y-devtools/frontend/src/lib/transitions.ts +34 -0
- data/gems/e11y-devtools/frontend/src/lib/viewportOrigin.ts +25 -0
- data/gems/e11y-devtools/frontend/src/main.ts +8 -0
- data/gems/e11y-devtools/frontend/src/overlay-entry.ts +24 -0
- data/gems/e11y-devtools/frontend/src/overlay.css +1080 -0
- data/gems/e11y-devtools/frontend/svelte.config.js +2 -0
- data/gems/e11y-devtools/frontend/test_puppeteer.js +41 -0
- data/gems/e11y-devtools/frontend/test_scale.js +3 -0
- data/gems/e11y-devtools/frontend/tsconfig.app.json +21 -0
- data/gems/e11y-devtools/frontend/tsconfig.json +7 -0
- data/gems/e11y-devtools/frontend/tsconfig.node.json +26 -0
- data/gems/e11y-devtools/frontend/vite.config.ts +36 -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 +20 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +94 -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 +67 -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 +91 -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 +44 -12
- data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
- data/lib/e11y/instruments/sidekiq.rb +135 -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 +4 -4
- data/lib/e11y/presets/audit_event.rb +13 -2
- data/lib/e11y/railtie.rb +52 -14
- 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 +144 -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 +123 -266
- 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 +186 -39
- data/docs/ADR-003-slo-observability.md +0 -3337
- 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
|
@@ -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
|
|
@@ -30,7 +30,7 @@ module E11y
|
|
|
30
30
|
#
|
|
31
31
|
# @example Critical Event Bypass (C02)
|
|
32
32
|
# # Payment events bypass rate limiting → DLQ if limited
|
|
33
|
-
# config.dlq_filter.
|
|
33
|
+
# config.dlq_filter.should_save?(event_data) # Event DSL: use_dlq
|
|
34
34
|
#
|
|
35
35
|
# # Result: Rate-limited payment events go to DLQ, not dropped
|
|
36
36
|
#
|
|
@@ -40,22 +40,33 @@ module E11y
|
|
|
40
40
|
# Initialize rate limiting middleware
|
|
41
41
|
#
|
|
42
42
|
# @param app [Object] Next middleware in pipeline
|
|
43
|
-
# @param global_limit [Integer] Max events/sec globally (default:
|
|
44
|
-
# @param per_event_limit [Integer] Max events/sec per event type (default:
|
|
45
|
-
# @param window [Float] Time window in seconds (default:
|
|
46
|
-
def initialize(app, global_limit:
|
|
43
|
+
# @param global_limit [Integer] Max events/sec globally (default: from E11y.config)
|
|
44
|
+
# @param per_event_limit [Integer] Max events/sec per event type (default: from E11y.config)
|
|
45
|
+
# @param window [Float] Time window in seconds (default: from E11y.config)
|
|
46
|
+
def initialize(app, global_limit: nil, per_event_limit: nil, window: nil)
|
|
47
47
|
super(app)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
config = E11y.config
|
|
49
|
+
# When explicit limits are passed (e.g. from pipeline options), enable for this instance
|
|
50
|
+
explicit_opts = global_limit || per_event_limit || window
|
|
51
|
+
@enabled = explicit_opts ? true : config.rate_limiting_enabled
|
|
52
|
+
@global_limit = global_limit || config.rate_limiting_global_limit
|
|
53
|
+
@global_window = window || config.rate_limiting_global_window
|
|
54
|
+
@window = @global_window # Alias for spec compatibility
|
|
55
|
+
@per_event_limit = per_event_limit || config.rate_limiting_per_event_limit
|
|
56
|
+
@explicit_per_event = per_event_limit && window
|
|
51
57
|
|
|
52
58
|
# Token buckets for rate limiting
|
|
53
|
-
@global_bucket = TokenBucket.new(
|
|
59
|
+
@global_bucket = TokenBucket.new(
|
|
60
|
+
capacity: @global_limit,
|
|
61
|
+
refill_rate: @global_limit,
|
|
62
|
+
window: @global_window
|
|
63
|
+
)
|
|
54
64
|
@per_event_buckets = Hash.new do |hash, event_name|
|
|
65
|
+
limit_cfg = @explicit_per_event ? { limit: @per_event_limit, window: @window } : config.rate_limit_for(event_name)
|
|
55
66
|
hash[event_name] = TokenBucket.new(
|
|
56
|
-
capacity:
|
|
57
|
-
refill_rate:
|
|
58
|
-
window:
|
|
67
|
+
capacity: limit_cfg[:limit],
|
|
68
|
+
refill_rate: limit_cfg[:limit],
|
|
69
|
+
window: limit_cfg[:window]
|
|
59
70
|
)
|
|
60
71
|
end
|
|
61
72
|
|
|
@@ -67,6 +78,8 @@ module E11y
|
|
|
67
78
|
# @param event_data [Hash] Event payload
|
|
68
79
|
# @return [Hash, nil] Event data if allowed, nil if rate limited
|
|
69
80
|
def call(event_data)
|
|
81
|
+
return @app.call(event_data) unless @enabled
|
|
82
|
+
|
|
70
83
|
event_name = event_data[:event_name]
|
|
71
84
|
|
|
72
85
|
# Check global rate limit
|
|
@@ -83,7 +96,7 @@ module E11y
|
|
|
83
96
|
end
|
|
84
97
|
|
|
85
98
|
# Rate limit not exceeded - continue pipeline
|
|
86
|
-
event_data
|
|
99
|
+
@app.call(event_data)
|
|
87
100
|
end
|
|
88
101
|
|
|
89
102
|
private
|
|
@@ -97,16 +110,31 @@ module E11y
|
|
|
97
110
|
def handle_rate_limited(event_data, limit_type)
|
|
98
111
|
event_name = event_data[:event_name]
|
|
99
112
|
|
|
100
|
-
# Log rate limiting
|
|
101
|
-
warn
|
|
113
|
+
# Log rate limiting (via E11y.logger so it respects Rails.logger in test env)
|
|
114
|
+
E11y.logger&.warn("[E11y] Rate limit exceeded (#{limit_type}) for event: #{event_name}")
|
|
102
115
|
|
|
103
116
|
# C02 Resolution: Check if event should be saved to DLQ
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
if should_save_to_dlq?(event_data)
|
|
118
|
+
record_dropped_metric(event_data, "rate_limited_#{limit_type}_dlq")
|
|
119
|
+
save_to_dlq(event_data, limit_type)
|
|
120
|
+
else
|
|
121
|
+
record_dropped_metric(event_data, "rate_limited_#{limit_type}")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
# Record e11y_events_dropped_total metric (non-fatal, safe when Metrics unavailable)
|
|
126
|
+
#
|
|
127
|
+
# @param event_data [Hash] Event payload
|
|
128
|
+
# @param reason [String] Drop reason (e.g., sampled_out, rate_limited_global)
|
|
129
|
+
def record_dropped_metric(event_data, reason)
|
|
130
|
+
return unless defined?(E11y::Metrics) && E11y::Metrics.respond_to?(:increment)
|
|
131
|
+
|
|
132
|
+
E11y::Metrics.increment(:e11y_events_dropped_total, {
|
|
133
|
+
reason: reason,
|
|
134
|
+
event_type: event_data[:event_name].to_s
|
|
135
|
+
})
|
|
136
|
+
rescue StandardError
|
|
137
|
+
# non-fatal
|
|
110
138
|
end
|
|
111
139
|
|
|
112
140
|
# Check if rate-limited event should be saved to DLQ (C02 Resolution)
|
|
@@ -120,9 +148,8 @@ module E11y
|
|
|
120
148
|
dlq_filter = E11y.config.dlq_filter
|
|
121
149
|
return false unless dlq_filter
|
|
122
150
|
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
dlq_filter.always_save_patterns&.any? { |pattern| pattern.match?(event_name) }
|
|
151
|
+
# Use DLQ filter (Event DSL: use_dlq, severity, default)
|
|
152
|
+
dlq_filter.should_save?(event_data)
|
|
126
153
|
end
|
|
127
154
|
|
|
128
155
|
# Save rate-limited critical event to DLQ (C02 Resolution)
|
|
@@ -135,19 +162,19 @@ module E11y
|
|
|
135
162
|
dlq_storage = E11y.config.dlq_storage
|
|
136
163
|
return unless dlq_storage
|
|
137
164
|
|
|
165
|
+
per_event_limit = limit_type == :per_event ? E11y.config.rate_limit_for(event_data[:event_name])[:limit] : @per_event_limit
|
|
138
166
|
dlq_storage.save(event_data, metadata: {
|
|
139
167
|
reason: "rate_limited_#{limit_type}",
|
|
140
168
|
limit_type: limit_type,
|
|
141
169
|
global_limit: @global_limit,
|
|
142
|
-
per_event_limit:
|
|
170
|
+
per_event_limit: per_event_limit,
|
|
143
171
|
timestamp: Time.now.utc.iso8601
|
|
144
172
|
})
|
|
145
173
|
|
|
146
|
-
warn
|
|
147
|
-
# TODO: Track metric e11y.rate_limiter.dlq_saved
|
|
174
|
+
E11y.logger&.warn("[E11y] Rate-limited critical event saved to DLQ: #{event_data[:event_name]}")
|
|
148
175
|
rescue StandardError => e
|
|
149
176
|
# Don't fail if DLQ save fails (C18 Resolution)
|
|
150
|
-
warn
|
|
177
|
+
E11y.logger&.warn("[E11y] Failed to save rate-limited event to DLQ: #{e.message}")
|
|
151
178
|
end
|
|
152
179
|
|
|
153
180
|
# Token Bucket implementation for rate limiting
|