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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require_relative "base"
|
|
7
|
+
|
|
8
|
+
module E11y
|
|
9
|
+
module Reliability
|
|
10
|
+
module DLQ
|
|
11
|
+
# File-based Dead Letter Queue storage.
|
|
12
|
+
#
|
|
13
|
+
# Stores failed events to a JSONL file for later analysis/replay.
|
|
14
|
+
# Each line is a JSON object representing a failed event with metadata.
|
|
15
|
+
#
|
|
16
|
+
# @example Usage
|
|
17
|
+
# dlq = FileAdapter.new(file_path: "log/e11y_dlq.jsonl")
|
|
18
|
+
# dlq.save(event_data, metadata: { error: "Timeout", retry_count: 3 })
|
|
19
|
+
#
|
|
20
|
+
# @see ADR-013 §4 (Dead Letter Queue)
|
|
21
|
+
# @see UC-021 §3 (DLQ File Storage)
|
|
22
|
+
# rubocop:disable Metrics/ClassLength
|
|
23
|
+
# DLQ file storage is a cohesive unit handling event persistence, rotation, and querying
|
|
24
|
+
class FileAdapter < Base
|
|
25
|
+
# @param file_path [String] Path to DLQ file (default: log/e11y_dlq.jsonl)
|
|
26
|
+
# @param max_file_size_mb [Integer] Maximum file size in MB before rotation (default: 100)
|
|
27
|
+
# @param retention_days [Integer] Days to retain DLQ files (default: 30)
|
|
28
|
+
def initialize(file_path: nil, max_file_size_mb: 100, retention_days: 30)
|
|
29
|
+
super()
|
|
30
|
+
@file_path = file_path || default_file_path
|
|
31
|
+
@max_file_size_bytes = max_file_size_mb * 1024 * 1024
|
|
32
|
+
@retention_days = retention_days
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
|
|
35
|
+
ensure_directory_exists
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Save failed event to DLQ.
|
|
39
|
+
#
|
|
40
|
+
# @param event_data [Hash] Event data
|
|
41
|
+
# @param metadata [Hash] Failure metadata (error, retry_count, adapter, etc.)
|
|
42
|
+
# @return [String] Event ID (UUID)
|
|
43
|
+
# DLQ save requires building entry, writing, rotation, cleanup, and metrics
|
|
44
|
+
def save(event_data, metadata: {})
|
|
45
|
+
event_id = SecureRandom.uuid
|
|
46
|
+
timestamp = Time.now.utc
|
|
47
|
+
|
|
48
|
+
dlq_entry = {
|
|
49
|
+
id: event_id,
|
|
50
|
+
timestamp: timestamp.iso8601(3),
|
|
51
|
+
event_name: event_data[:event_name],
|
|
52
|
+
event_data: event_data,
|
|
53
|
+
metadata: metadata.merge(
|
|
54
|
+
failed_at: timestamp.iso8601(3),
|
|
55
|
+
retry_count: metadata[:retry_count] || 0,
|
|
56
|
+
error_message: metadata[:error]&.message,
|
|
57
|
+
error_class: metadata[:error]&.class&.name
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
write_entry(dlq_entry)
|
|
62
|
+
rotate_if_needed
|
|
63
|
+
cleanup_old_files
|
|
64
|
+
|
|
65
|
+
increment_metric("e11y.dlq.saved", event_name: event_data[:event_name])
|
|
66
|
+
|
|
67
|
+
event_id
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# List DLQ entries with optional filters.
|
|
71
|
+
#
|
|
72
|
+
# @param limit [Integer] Maximum entries to return
|
|
73
|
+
# @param offset [Integer] Number of entries to skip
|
|
74
|
+
# @param filters [Hash] Filter options (event_name, after, before)
|
|
75
|
+
# @return [Array<Hash>] Array of DLQ entries
|
|
76
|
+
# rubocop:disable Metrics/AbcSize
|
|
77
|
+
# DLQ listing requires file iteration, pagination, multiple filters, and error handling
|
|
78
|
+
def list(limit: 100, offset: 0, filters: {})
|
|
79
|
+
entries = []
|
|
80
|
+
|
|
81
|
+
return entries unless File.exist?(@file_path)
|
|
82
|
+
|
|
83
|
+
File.foreach(@file_path).with_index do |line, index|
|
|
84
|
+
next if index < offset
|
|
85
|
+
break if entries.size >= limit
|
|
86
|
+
|
|
87
|
+
entry = JSON.parse(line, symbolize_names: true)
|
|
88
|
+
|
|
89
|
+
# Apply filters
|
|
90
|
+
next if filters[:event_name] && entry[:event_name] != filters[:event_name]
|
|
91
|
+
next if filters[:after] && Time.parse(entry[:timestamp]) < filters[:after]
|
|
92
|
+
next if filters[:before] && Time.parse(entry[:timestamp]) > filters[:before]
|
|
93
|
+
|
|
94
|
+
entries << entry
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
entries
|
|
98
|
+
rescue JSON::ParserError => e
|
|
99
|
+
# Log parsing error but don't crash
|
|
100
|
+
increment_metric("e11y.dlq.parse_error", error: e.class.name)
|
|
101
|
+
entries
|
|
102
|
+
end
|
|
103
|
+
# rubocop:enable Metrics/AbcSize
|
|
104
|
+
|
|
105
|
+
# Get DLQ statistics.
|
|
106
|
+
#
|
|
107
|
+
# @return [Hash] Statistics (total_entries, file_size_mb, oldest_entry, newest_entry)
|
|
108
|
+
# rubocop:disable Metrics/AbcSize
|
|
109
|
+
# DLQ stats requires reading file size, counting entries, extracting timestamps, and error handling
|
|
110
|
+
def stats
|
|
111
|
+
return default_stats unless File.exist?(@file_path)
|
|
112
|
+
|
|
113
|
+
file_size_bytes = File.size(@file_path)
|
|
114
|
+
total_entries = File.foreach(@file_path).count
|
|
115
|
+
|
|
116
|
+
oldest_entry = nil
|
|
117
|
+
newest_entry = nil
|
|
118
|
+
|
|
119
|
+
# Read first and last line for oldest/newest timestamps
|
|
120
|
+
File.foreach(@file_path).with_index do |line, index|
|
|
121
|
+
entry = JSON.parse(line, symbolize_names: true)
|
|
122
|
+
oldest_entry = entry[:timestamp] if index.zero?
|
|
123
|
+
newest_entry = entry[:timestamp]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
total_entries: total_entries,
|
|
128
|
+
file_size_mb: (file_size_bytes / 1024.0 / 1024.0).round(2),
|
|
129
|
+
oldest_entry: oldest_entry,
|
|
130
|
+
newest_entry: newest_entry,
|
|
131
|
+
file_path: @file_path
|
|
132
|
+
}
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
increment_metric("e11y.dlq.stats_error", error: e.class.name)
|
|
135
|
+
default_stats
|
|
136
|
+
end
|
|
137
|
+
# rubocop:enable Metrics/AbcSize
|
|
138
|
+
|
|
139
|
+
# Replay single event from DLQ.
|
|
140
|
+
#
|
|
141
|
+
# Re-dispatches the stored event_data to all registered adapters so the
|
|
142
|
+
# event reaches every adapter regardless of original routing configuration.
|
|
143
|
+
#
|
|
144
|
+
# NOTE: Delivers directly to adapters, bypassing the middleware pipeline.
|
|
145
|
+
# This means middleware (PII filtering, rate limiting, routing) is NOT applied.
|
|
146
|
+
# Trade-off: ensures event reaches all registered adapters regardless of routing rules.
|
|
147
|
+
#
|
|
148
|
+
# @param event_id [String] Event ID to replay
|
|
149
|
+
# @return [Boolean] true if replayed successfully
|
|
150
|
+
def replay(event_id)
|
|
151
|
+
entry = find_entry(event_id)
|
|
152
|
+
return false unless entry
|
|
153
|
+
|
|
154
|
+
# Reconstruct event_data from stored entry (keys are symbolized after JSON parse)
|
|
155
|
+
event_data = entry[:event_data]
|
|
156
|
+
|
|
157
|
+
# Deliver directly to all registered adapters, bypassing routing.
|
|
158
|
+
# This ensures the replayed event reaches every adapter (including
|
|
159
|
+
# adapters added after the original event was stored in the DLQ).
|
|
160
|
+
E11y.configuration.adapters.each_value do |adapter|
|
|
161
|
+
adapter.write(event_data)
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
E11y.logger.error("DLQ replay write failed: #{e.message}")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
increment_metric("e11y.dlq.replayed", event_name: entry[:event_name])
|
|
167
|
+
true
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
increment_metric("e11y.dlq.replay_failed", error: e.class.name)
|
|
170
|
+
false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Delete entry from DLQ.
|
|
174
|
+
#
|
|
175
|
+
# Rewrites the JSONL file excluding the line whose :id matches event_id.
|
|
176
|
+
# Returns true if an entry was found and removed, false otherwise.
|
|
177
|
+
#
|
|
178
|
+
# @param event_id [String] Event ID to delete
|
|
179
|
+
# @return [Boolean] true if deleted
|
|
180
|
+
# delete is an action method returning boolean status, not a predicate query
|
|
181
|
+
# rubocop:disable Metrics/AbcSize -- file rewrite with mutex, filters, error handling
|
|
182
|
+
def delete(event_id)
|
|
183
|
+
return false unless File.exist?(@file_path)
|
|
184
|
+
|
|
185
|
+
deleted = false
|
|
186
|
+
@mutex.synchronize do
|
|
187
|
+
all_lines = File.readlines(@file_path).map(&:chomp).reject(&:empty?)
|
|
188
|
+
original_count = all_lines.length
|
|
189
|
+
|
|
190
|
+
remaining = all_lines.reject do |line|
|
|
191
|
+
entry = JSON.parse(line, symbolize_names: true)
|
|
192
|
+
entry[:id].to_s == event_id.to_s
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if remaining.length < original_count
|
|
196
|
+
content = remaining.join("\n")
|
|
197
|
+
content += "\n" unless content.empty? || content.end_with?("\n")
|
|
198
|
+
File.write(@file_path, content)
|
|
199
|
+
deleted = true
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
deleted
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
E11y.logger.error("DLQ delete failed for #{event_id}: #{e.message}")
|
|
205
|
+
false
|
|
206
|
+
end
|
|
207
|
+
# rubocop:enable Metrics/AbcSize
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
# Get default file path (log/e11y_dlq.jsonl).
|
|
212
|
+
def default_file_path
|
|
213
|
+
::Rails.root.join("log", "e11y_dlq.jsonl").to_s
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Ensure log directory exists.
|
|
217
|
+
def ensure_directory_exists
|
|
218
|
+
dir = File.dirname(@file_path)
|
|
219
|
+
FileUtils.mkdir_p(dir)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Write DLQ entry to file (thread-safe).
|
|
223
|
+
def write_entry(entry)
|
|
224
|
+
@mutex.synchronize do
|
|
225
|
+
File.open(@file_path, "a") do |f|
|
|
226
|
+
f.flock(File::LOCK_EX)
|
|
227
|
+
f.puts(JSON.generate(entry))
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Rotate file if size exceeds max_file_size.
|
|
233
|
+
def rotate_if_needed
|
|
234
|
+
return unless File.exist?(@file_path)
|
|
235
|
+
return if File.size(@file_path) < @max_file_size_bytes
|
|
236
|
+
|
|
237
|
+
@mutex.synchronize do
|
|
238
|
+
# Rotate: log/e11y_dlq.jsonl → log/e11y_dlq.2026-01-20T12:34:56Z.jsonl
|
|
239
|
+
timestamp = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
240
|
+
rotated_path = @file_path.sub(/\.jsonl$/, ".#{timestamp}.jsonl")
|
|
241
|
+
|
|
242
|
+
FileUtils.mv(@file_path, rotated_path)
|
|
243
|
+
|
|
244
|
+
increment_metric("e11y.dlq.rotated", new_file: rotated_path)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Cleanup old rotated files.
|
|
249
|
+
def cleanup_old_files
|
|
250
|
+
dir = File.dirname(@file_path)
|
|
251
|
+
base_name = File.basename(@file_path, ".jsonl")
|
|
252
|
+
|
|
253
|
+
# Find all rotated files: e11y_dlq.*.jsonl
|
|
254
|
+
pattern = File.join(dir, "#{base_name}.*.jsonl")
|
|
255
|
+
|
|
256
|
+
Dir.glob(pattern).each do |file|
|
|
257
|
+
next unless File.file?(file)
|
|
258
|
+
|
|
259
|
+
file_age_days = (Time.now - File.mtime(file)) / 86_400
|
|
260
|
+
|
|
261
|
+
if file_age_days > @retention_days
|
|
262
|
+
File.delete(file)
|
|
263
|
+
increment_metric("e11y.dlq.cleaned_up", file: file)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Find DLQ entry by ID.
|
|
269
|
+
def find_entry(event_id)
|
|
270
|
+
return nil unless File.exist?(@file_path)
|
|
271
|
+
|
|
272
|
+
File.foreach(@file_path) do |line|
|
|
273
|
+
entry = JSON.parse(line, symbolize_names: true)
|
|
274
|
+
return entry if entry[:id] == event_id
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
nil
|
|
278
|
+
rescue JSON::ParserError
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Default stats when file doesn't exist.
|
|
283
|
+
def default_stats
|
|
284
|
+
{
|
|
285
|
+
total_entries: 0,
|
|
286
|
+
file_size_mb: 0.0,
|
|
287
|
+
oldest_entry: nil,
|
|
288
|
+
newest_entry: nil,
|
|
289
|
+
file_path: @file_path
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Increment DLQ metric (no-op when E11y::Metrics backend not configured).
|
|
294
|
+
def increment_metric(metric_name, tags = {})
|
|
295
|
+
E11y::Metrics.increment(metric_name, **tags)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
# rubocop:enable Metrics/ClassLength
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
@@ -38,7 +38,6 @@ module E11y
|
|
|
38
38
|
# @param event_data [Hash] Event data
|
|
39
39
|
# @param metadata [Hash] Failure metadata (error, retry_count, adapter, etc.)
|
|
40
40
|
# @return [String] Event ID (UUID)
|
|
41
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
42
41
|
# DLQ save requires building entry, writing, rotation, cleanup, and metrics
|
|
43
42
|
def save(event_data, metadata: {})
|
|
44
43
|
event_id = SecureRandom.uuid
|
|
@@ -61,11 +60,11 @@ module E11y
|
|
|
61
60
|
rotate_if_needed
|
|
62
61
|
cleanup_old_files
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
E11y::Metrics.increment("e11y.dlq.saved", event_name: event_data[:event_name])
|
|
64
|
+
update_dlq_size_gauge
|
|
65
65
|
|
|
66
66
|
event_id
|
|
67
67
|
end
|
|
68
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
69
68
|
|
|
70
69
|
# List DLQ entries with optional filters.
|
|
71
70
|
#
|
|
@@ -73,7 +72,7 @@ module E11y
|
|
|
73
72
|
# @param offset [Integer] Number of entries to skip
|
|
74
73
|
# @param filters [Hash] Filter options (event_name, after, before)
|
|
75
74
|
# @return [Array<Hash>] Array of DLQ entries
|
|
76
|
-
# rubocop:disable Metrics/AbcSize
|
|
75
|
+
# rubocop:disable Metrics/AbcSize
|
|
77
76
|
# DLQ listing requires file iteration, pagination, multiple filters, and error handling
|
|
78
77
|
def list(limit: 100, offset: 0, filters: {})
|
|
79
78
|
entries = []
|
|
@@ -97,15 +96,14 @@ module E11y
|
|
|
97
96
|
entries
|
|
98
97
|
rescue JSON::ParserError => e
|
|
99
98
|
# Log parsing error but don't crash
|
|
100
|
-
|
|
99
|
+
E11y::Metrics.increment("e11y.dlq.parse_error", error: e.class.name)
|
|
101
100
|
entries
|
|
102
101
|
end
|
|
103
|
-
# rubocop:enable Metrics/AbcSize
|
|
102
|
+
# rubocop:enable Metrics/AbcSize
|
|
104
103
|
|
|
105
104
|
# Get DLQ statistics.
|
|
106
105
|
#
|
|
107
106
|
# @return [Hash] Statistics (total_entries, file_size_mb, oldest_entry, newest_entry)
|
|
108
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
109
107
|
# DLQ stats requires reading file size, counting entries, extracting timestamps, and error handling
|
|
110
108
|
def stats
|
|
111
109
|
return default_stats unless File.exist?(@file_path)
|
|
@@ -130,29 +128,32 @@ module E11y
|
|
|
130
128
|
newest_entry: newest_entry,
|
|
131
129
|
file_path: @file_path
|
|
132
130
|
}
|
|
133
|
-
rescue StandardError
|
|
134
|
-
increment_metric("e11y.dlq.stats_error", error: e.class.name)
|
|
131
|
+
rescue StandardError
|
|
135
132
|
default_stats
|
|
136
133
|
end
|
|
137
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
138
134
|
|
|
139
135
|
# Replay single event from DLQ.
|
|
140
136
|
#
|
|
137
|
+
# Re-dispatches event through E11y pipeline so it reaches adapters.
|
|
138
|
+
#
|
|
141
139
|
# @param event_id [String] Event ID to replay
|
|
142
140
|
# @return [Boolean] true if replayed successfully
|
|
143
141
|
def replay(event_id)
|
|
144
142
|
entry = find_entry(event_id)
|
|
145
143
|
return false unless entry
|
|
146
144
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
# E11y::Pipeline.dispatch(entry[:event_data], metadata: entry[:metadata].merge(replayed: true))
|
|
145
|
+
event_data = entry[:event_data]
|
|
146
|
+
return false unless event_data
|
|
150
147
|
|
|
151
|
-
#
|
|
152
|
-
|
|
148
|
+
# F-004/C07: Mark as DLQ-replayed so PIIFilter skips (avoid double-hashing)
|
|
149
|
+
event_data = event_data.dup
|
|
150
|
+
event_data[:dlq_replayed] = true
|
|
151
|
+
|
|
152
|
+
E11y.config.built_pipeline.call(event_data)
|
|
153
|
+
E11y::Metrics.increment("e11y.dlq.replayed", event_name: entry[:event_name])
|
|
153
154
|
true
|
|
154
155
|
rescue StandardError => e
|
|
155
|
-
|
|
156
|
+
E11y::Metrics.increment("e11y.dlq.replay_failed", error: e.class.name)
|
|
156
157
|
false
|
|
157
158
|
end
|
|
158
159
|
|
|
@@ -177,23 +178,58 @@ module E11y
|
|
|
177
178
|
|
|
178
179
|
# Delete entry from DLQ.
|
|
179
180
|
#
|
|
180
|
-
#
|
|
181
|
-
# In production, consider using a database or append-only log with tombstones.
|
|
181
|
+
# Rewrites file excluding the entry. For large files this is expensive.
|
|
182
182
|
#
|
|
183
183
|
# @param event_id [String] Event ID to delete
|
|
184
184
|
# @return [Boolean] true if deleted
|
|
185
|
-
# rubocop:disable Naming/PredicateMethod
|
|
186
185
|
# delete is an action method returning boolean status, not a predicate query
|
|
187
|
-
def delete(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
def delete(event_id)
|
|
187
|
+
return false unless File.exist?(@file_path)
|
|
188
|
+
|
|
189
|
+
entries, found = read_entries_excluding(event_id)
|
|
190
|
+
return false unless found
|
|
191
|
+
|
|
192
|
+
rewrite_file_with(entries)
|
|
193
|
+
update_dlq_size_gauge
|
|
194
|
+
true
|
|
195
|
+
rescue StandardError
|
|
191
196
|
false
|
|
192
197
|
end
|
|
193
|
-
# rubocop:enable Naming/PredicateMethod
|
|
194
198
|
|
|
195
199
|
private
|
|
196
200
|
|
|
201
|
+
def update_dlq_size_gauge
|
|
202
|
+
return unless defined?(E11y::Metrics) && E11y::Metrics.respond_to?(:gauge)
|
|
203
|
+
|
|
204
|
+
count = stats[:total_entries]
|
|
205
|
+
E11y::Metrics.gauge(:e11y_dlq_size, count)
|
|
206
|
+
rescue StandardError
|
|
207
|
+
# Non-fatal: gauge update must not break DLQ operations
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def read_entries_excluding(event_id)
|
|
211
|
+
entries = []
|
|
212
|
+
found = false
|
|
213
|
+
File.foreach(@file_path) do |line|
|
|
214
|
+
entry = JSON.parse(line, symbolize_names: true)
|
|
215
|
+
if entry[:id] == event_id
|
|
216
|
+
found = true
|
|
217
|
+
else
|
|
218
|
+
entries << entry
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
[entries, found]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def rewrite_file_with(entries)
|
|
225
|
+
@mutex.synchronize do
|
|
226
|
+
File.open(@file_path, "w") do |f|
|
|
227
|
+
f.flock(File::LOCK_EX)
|
|
228
|
+
entries.each { |e| f.puts(JSON.generate(e)) }
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
197
233
|
# Get default file path (log/e11y_dlq.jsonl).
|
|
198
234
|
def default_file_path
|
|
199
235
|
::Rails.root.join("log", "e11y_dlq.jsonl").to_s
|
|
@@ -226,8 +262,6 @@ module E11y
|
|
|
226
262
|
rotated_path = @file_path.sub(/\.jsonl$/, ".#{timestamp}.jsonl")
|
|
227
263
|
|
|
228
264
|
FileUtils.mv(@file_path, rotated_path)
|
|
229
|
-
|
|
230
|
-
increment_metric("e11y.dlq.rotated", new_file: rotated_path)
|
|
231
265
|
end
|
|
232
266
|
end
|
|
233
267
|
|
|
@@ -244,10 +278,7 @@ module E11y
|
|
|
244
278
|
|
|
245
279
|
file_age_days = (Time.now - File.mtime(file)) / 86_400
|
|
246
280
|
|
|
247
|
-
if file_age_days > @retention_days
|
|
248
|
-
File.delete(file)
|
|
249
|
-
increment_metric("e11y.dlq.cleaned_up", file: file)
|
|
250
|
-
end
|
|
281
|
+
File.delete(file) if file_age_days > @retention_days
|
|
251
282
|
end
|
|
252
283
|
end
|
|
253
284
|
|
|
@@ -277,10 +308,8 @@ module E11y
|
|
|
277
308
|
end
|
|
278
309
|
|
|
279
310
|
# Increment DLQ metric.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# E11y::Metrics.increment(metric_name, tags)
|
|
283
|
-
end
|
|
311
|
+
#
|
|
312
|
+
# Normalizes metric_name like "e11y.dlq.saved" to :e11y_dlq_saved_total.
|
|
284
313
|
end
|
|
285
314
|
# rubocop:enable Metrics/ClassLength
|
|
286
315
|
end
|
|
@@ -5,91 +5,79 @@ module E11y
|
|
|
5
5
|
module DLQ
|
|
6
6
|
# DLQ Filter determines which failed events should be saved to DLQ.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# - Always discard patterns (e.g., debug.*, test.*)
|
|
11
|
-
# - Severity-based filtering (e.g., always save :error, :fatal)
|
|
8
|
+
# Uses Event DSL (use_dlq) when event class is registered.
|
|
9
|
+
# Audit events (Presets::AuditEvent) have use_dlq true by default.
|
|
12
10
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# )
|
|
11
|
+
# Priority order:
|
|
12
|
+
# 1. Event class use_dlq == false → discard
|
|
13
|
+
# 2. Event class use_dlq == true → save
|
|
14
|
+
# 3. Severity-based (save_severities)
|
|
15
|
+
# 4. Default behavior
|
|
19
16
|
#
|
|
20
|
-
#
|
|
17
|
+
# @example Event DSL
|
|
18
|
+
# class Events::AuditLogin < E11y::Events::BaseAuditEvent
|
|
19
|
+
# # use_dlq true from preset
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# class Events::DebugTrace < E11y::Event::Base
|
|
23
|
+
# use_dlq false
|
|
24
|
+
# end
|
|
21
25
|
#
|
|
22
26
|
# @see ADR-013 §4.3 (DLQ Filter)
|
|
23
27
|
# @see UC-021 §3.2 (DLQ Filter Configuration)
|
|
24
28
|
class Filter
|
|
25
|
-
# @param always_save_patterns [Array<Regexp>] Event patterns to always save
|
|
26
|
-
# @param always_discard_patterns [Array<Regexp>] Event patterns to always discard
|
|
27
29
|
# @param save_severities [Array<Symbol>] Severities to always save (:error, :fatal)
|
|
28
|
-
# @param default_behavior [Symbol] Default
|
|
30
|
+
# @param default_behavior [Symbol] Default when no Event DSL rule (:save or :discard)
|
|
29
31
|
def initialize(
|
|
30
|
-
always_save_patterns: [],
|
|
31
|
-
always_discard_patterns: [],
|
|
32
32
|
save_severities: %i[error fatal],
|
|
33
33
|
default_behavior: :save
|
|
34
34
|
)
|
|
35
|
-
@always_save_patterns = always_save_patterns
|
|
36
|
-
@always_discard_patterns = always_discard_patterns
|
|
37
35
|
@save_severities = save_severities
|
|
38
36
|
@default_behavior = default_behavior
|
|
39
37
|
end
|
|
40
38
|
|
|
41
39
|
# Check if event should be saved to DLQ.
|
|
42
40
|
#
|
|
43
|
-
# Priority order:
|
|
44
|
-
# 1. Always discard patterns (highest priority)
|
|
45
|
-
# 2. Always save patterns
|
|
46
|
-
# 3. Severity-based rules
|
|
47
|
-
# 4. Default behavior
|
|
48
|
-
#
|
|
49
41
|
# @param event_data [Hash] Event data
|
|
42
|
+
# @param error [StandardError, nil] The error that caused the DLQ save (optional)
|
|
50
43
|
# @return [Boolean] true if event should be saved to DLQ
|
|
51
44
|
# rubocop:disable Metrics/MethodLength
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
event_name = event_data[:event_name].to_s
|
|
45
|
+
def should_save?(event_data, _error = nil)
|
|
46
|
+
event_class = resolve_event_class(event_data[:event_name])
|
|
55
47
|
severity = event_data[:severity]
|
|
56
48
|
|
|
57
|
-
# Priority 1:
|
|
58
|
-
if
|
|
59
|
-
|
|
49
|
+
# Priority 1: Event DSL use_dlq == false
|
|
50
|
+
if event_class.respond_to?(:use_dlq) && event_class.use_dlq == false
|
|
51
|
+
increment_filter_metric("discarded", "use_dlq")
|
|
60
52
|
return false
|
|
61
53
|
end
|
|
62
54
|
|
|
63
|
-
# Priority 2:
|
|
64
|
-
if
|
|
65
|
-
|
|
55
|
+
# Priority 2: Event DSL use_dlq == true
|
|
56
|
+
if event_class.respond_to?(:use_dlq) && event_class.use_dlq == true
|
|
57
|
+
increment_filter_metric("saved", "use_dlq")
|
|
66
58
|
return true
|
|
67
59
|
end
|
|
68
60
|
|
|
69
61
|
# Priority 3: Severity-based
|
|
70
62
|
if @save_severities.include?(severity)
|
|
71
|
-
|
|
63
|
+
increment_filter_metric("saved", "severity")
|
|
72
64
|
return true
|
|
73
65
|
end
|
|
74
66
|
|
|
75
67
|
# Priority 4: Default behavior
|
|
76
68
|
if @default_behavior == :save
|
|
77
|
-
|
|
69
|
+
increment_filter_metric("saved", "default")
|
|
78
70
|
true
|
|
79
71
|
else
|
|
80
|
-
|
|
72
|
+
increment_filter_metric("discarded", "default")
|
|
81
73
|
false
|
|
82
74
|
end
|
|
83
75
|
end
|
|
84
76
|
# rubocop:enable Metrics/MethodLength
|
|
85
77
|
|
|
86
|
-
# Get filter statistics.
|
|
87
|
-
#
|
|
88
78
|
# @return [Hash] Filter configuration stats
|
|
89
79
|
def stats
|
|
90
80
|
{
|
|
91
|
-
always_save_patterns: @always_save_patterns.map(&:inspect),
|
|
92
|
-
always_discard_patterns: @always_discard_patterns.map(&:inspect),
|
|
93
81
|
save_severities: @save_severities,
|
|
94
82
|
default_behavior: @default_behavior
|
|
95
83
|
}
|
|
@@ -97,22 +85,17 @@ module E11y
|
|
|
97
85
|
|
|
98
86
|
private
|
|
99
87
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def matches_patterns?(event_name, patterns)
|
|
106
|
-
patterns.any? { |pattern| pattern.match?(event_name) }
|
|
88
|
+
def resolve_event_class(event_name)
|
|
89
|
+
return nil unless event_name
|
|
90
|
+
return nil unless defined?(E11y::Registry) && E11y::Registry.respond_to?(:find)
|
|
91
|
+
|
|
92
|
+
E11y::Registry.find(event_name.to_s)
|
|
107
93
|
end
|
|
108
94
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def increment_metric(metric_name, tags = {})
|
|
114
|
-
# TODO: Integrate with Yabeda metrics
|
|
115
|
-
# E11y::Metrics.increment(metric_name, tags)
|
|
95
|
+
def increment_filter_metric(action, reason)
|
|
96
|
+
return unless defined?(E11y::Metrics) && E11y::Metrics.respond_to?(:increment)
|
|
97
|
+
|
|
98
|
+
E11y::Metrics.increment(:e11y_dlq_filter_decisions_total, { action: action, reason: reason })
|
|
116
99
|
end
|
|
117
100
|
end
|
|
118
101
|
end
|