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,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
# Thread-safe auto-populating registry for discovering and inspecting all defined E11y event classes.
|
|
5
|
+
#
|
|
6
|
+
# Events are registered automatically when `event_name` is set on a subclass of `E11y::Event::Base`.
|
|
7
|
+
# The registry is always-on (no configuration needed) and is safe for concurrent use.
|
|
8
|
+
#
|
|
9
|
+
# @example Discover all events
|
|
10
|
+
# E11y::Registry.event_classes
|
|
11
|
+
# # => [Events::OrderCreated, Events::PaymentFailed, ...]
|
|
12
|
+
#
|
|
13
|
+
# @example Find an event class by name
|
|
14
|
+
# E11y::Registry.find("order.created")
|
|
15
|
+
# # => Events::OrderCreated
|
|
16
|
+
#
|
|
17
|
+
# @example Filter events by severity
|
|
18
|
+
# E11y::Registry.where(severity: :error)
|
|
19
|
+
# # => [Events::PaymentFailed, ...]
|
|
20
|
+
#
|
|
21
|
+
# @example Generate documentation
|
|
22
|
+
# E11y::Registry.to_documentation
|
|
23
|
+
# # => [{ name: "order.created", class: "Events::OrderCreated", ... }, ...]
|
|
24
|
+
#
|
|
25
|
+
# @see UC-022 Event Registry
|
|
26
|
+
class Registry
|
|
27
|
+
class << self
|
|
28
|
+
# Singleton instance
|
|
29
|
+
#
|
|
30
|
+
# @return [Registry]
|
|
31
|
+
def instance
|
|
32
|
+
@instance ||= new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Register an event class. Delegates to singleton instance.
|
|
36
|
+
#
|
|
37
|
+
# @param event_class [Class] Event class to register
|
|
38
|
+
# @return [void]
|
|
39
|
+
def register(event_class)
|
|
40
|
+
instance.register(event_class)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Find event class by name. Delegates to singleton instance.
|
|
44
|
+
#
|
|
45
|
+
# @param event_name [String] Event name
|
|
46
|
+
# @param version [Integer, nil] Specific version (nil = latest)
|
|
47
|
+
# @return [Class, nil] Event class or nil
|
|
48
|
+
def find(event_name, version: nil)
|
|
49
|
+
instance.find(event_name, version: version)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return all registered event classes. Delegates to singleton instance.
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<Class>]
|
|
55
|
+
def event_classes
|
|
56
|
+
instance.event_classes
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Filter events by criteria. Delegates to singleton instance.
|
|
60
|
+
#
|
|
61
|
+
# @param criteria [Hash] Filter criteria (:severity, :version, :adapter)
|
|
62
|
+
# @return [Array<Class>]
|
|
63
|
+
def where(**criteria)
|
|
64
|
+
instance.where(**criteria)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Validate that an event is properly configured. Delegates to singleton instance.
|
|
68
|
+
#
|
|
69
|
+
# @param event_name [String]
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def validate(event_name)
|
|
72
|
+
instance.validate(event_name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Clear all registered events. Delegates to singleton instance.
|
|
76
|
+
#
|
|
77
|
+
# @return [void]
|
|
78
|
+
def clear!
|
|
79
|
+
instance.clear!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Number of unique event names registered. Delegates to singleton instance.
|
|
83
|
+
#
|
|
84
|
+
# @return [Integer]
|
|
85
|
+
def size
|
|
86
|
+
instance.size
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Generate documentation hash for all events. Delegates to singleton instance.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<Hash>]
|
|
92
|
+
def to_documentation
|
|
93
|
+
instance.to_documentation
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Return all versions of an event (ADR-012 §6.2).
|
|
97
|
+
#
|
|
98
|
+
# @param event_name [String] Event name
|
|
99
|
+
# @return [Array<Hash>] [{ version: N, class: Klass }, ...] sorted by version
|
|
100
|
+
def all_versions(event_name)
|
|
101
|
+
instance.all_versions(event_name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Return event names that have multiple versions (ADR-012 §6.2).
|
|
105
|
+
#
|
|
106
|
+
# @return [Array<String>]
|
|
107
|
+
def versioned_events
|
|
108
|
+
instance.versioned_events
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Reset the singleton instance (primarily for test isolation).
|
|
112
|
+
#
|
|
113
|
+
# After calling this, the next call to `.instance` creates a fresh registry.
|
|
114
|
+
# Note: previously registered events will NOT be re-registered unless their
|
|
115
|
+
# class definitions are re-evaluated.
|
|
116
|
+
#
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @api private
|
|
119
|
+
def reset!
|
|
120
|
+
@instance = nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Initialize a new Registry instance.
|
|
125
|
+
#
|
|
126
|
+
# Creates an empty, thread-safe registry backed by a Mutex-protected Hash.
|
|
127
|
+
def initialize
|
|
128
|
+
@registry = {} # event_name (String) => Array<Class>
|
|
129
|
+
@mutex = Mutex.new
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Register an event class.
|
|
133
|
+
#
|
|
134
|
+
# Safe to call multiple times with the same class — idempotent.
|
|
135
|
+
# Silently ignores classes that do not respond to `event_name`
|
|
136
|
+
# or return a blank name (e.g. intermediate abstract classes).
|
|
137
|
+
#
|
|
138
|
+
# @param event_class [Class] Event class to register
|
|
139
|
+
# @return [void]
|
|
140
|
+
def register(event_class)
|
|
141
|
+
return unless event_class.respond_to?(:event_name)
|
|
142
|
+
|
|
143
|
+
name = begin
|
|
144
|
+
event_class.event_name
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return if name.nil? || name.empty? || name == "AnonymousEvent"
|
|
150
|
+
|
|
151
|
+
@mutex.synchronize do
|
|
152
|
+
@registry[name] ||= []
|
|
153
|
+
@registry[name] << event_class unless @registry[name].include?(event_class)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Find event class by name.
|
|
158
|
+
#
|
|
159
|
+
# When multiple classes share the same event name (versioning), returns the
|
|
160
|
+
# latest-registered one by default. Pass `version:` to find a specific version.
|
|
161
|
+
#
|
|
162
|
+
# @param event_name [String, Symbol] Event name to look up
|
|
163
|
+
# @param version [Integer, nil] Specific version number (nil = latest)
|
|
164
|
+
# @return [Class, nil] Matching event class or nil
|
|
165
|
+
def find(event_name, version: nil)
|
|
166
|
+
entries = @mutex.synchronize { @registry[event_name.to_s]&.dup }
|
|
167
|
+
return nil if entries.nil? || entries.empty?
|
|
168
|
+
|
|
169
|
+
if version
|
|
170
|
+
entries.find { |klass| klass.respond_to?(:version) && klass.version == version }
|
|
171
|
+
else
|
|
172
|
+
entries.last # latest registered = latest version
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Return all registered event classes as a flat array.
|
|
177
|
+
#
|
|
178
|
+
# The returned array is a copy — mutating it does not affect the registry.
|
|
179
|
+
#
|
|
180
|
+
# @return [Array<Class>]
|
|
181
|
+
def event_classes
|
|
182
|
+
@mutex.synchronize { @registry.values.flatten.dup }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Filter registered events by criteria.
|
|
186
|
+
#
|
|
187
|
+
# Supported criteria keys:
|
|
188
|
+
# - `:severity` — matches `klass.default_severity` or `klass.severity`
|
|
189
|
+
# - `:version` — matches `klass.version`
|
|
190
|
+
# - `:adapter` — matches if `klass.adapters` includes the value
|
|
191
|
+
#
|
|
192
|
+
# Unknown criteria keys always produce no matches (conservative).
|
|
193
|
+
#
|
|
194
|
+
# @param criteria [Hash]
|
|
195
|
+
# @return [Array<Class>]
|
|
196
|
+
def where(**criteria)
|
|
197
|
+
event_classes.select do |klass|
|
|
198
|
+
criteria.all? do |key, value|
|
|
199
|
+
case key
|
|
200
|
+
when :severity
|
|
201
|
+
# Support both default_severity and severity readers
|
|
202
|
+
reader = klass.respond_to?(:default_severity) ? :default_severity : :severity
|
|
203
|
+
klass.respond_to?(reader) && klass.public_send(reader) == value
|
|
204
|
+
when :version
|
|
205
|
+
klass.respond_to?(:version) && klass.version == value
|
|
206
|
+
when :adapter
|
|
207
|
+
klass.respond_to?(:adapters) && Array(klass.adapters).include?(value)
|
|
208
|
+
else
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Validate that a registered event has a compiled schema.
|
|
216
|
+
#
|
|
217
|
+
# Returns `false` for unknown events.
|
|
218
|
+
# Returns `true` if the class is registered and has a non-nil `compiled_schema`.
|
|
219
|
+
#
|
|
220
|
+
# @param event_name [String]
|
|
221
|
+
# @return [Boolean]
|
|
222
|
+
# rubocop:disable Naming/PredicateMethod -- "validate" is the established API name
|
|
223
|
+
def validate(event_name)
|
|
224
|
+
klass = find(event_name)
|
|
225
|
+
return false unless klass
|
|
226
|
+
|
|
227
|
+
klass.respond_to?(:compiled_schema) && !klass.compiled_schema.nil?
|
|
228
|
+
end
|
|
229
|
+
# rubocop:enable Naming/PredicateMethod
|
|
230
|
+
|
|
231
|
+
# Remove all entries from the registry.
|
|
232
|
+
#
|
|
233
|
+
# Primarily used in tests to avoid cross-test pollution.
|
|
234
|
+
#
|
|
235
|
+
# @return [void]
|
|
236
|
+
def clear!
|
|
237
|
+
@mutex.synchronize { @registry.clear }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Number of unique event names in the registry.
|
|
241
|
+
#
|
|
242
|
+
# Note: multiple versions of the same event name count as 1.
|
|
243
|
+
#
|
|
244
|
+
# @return [Integer]
|
|
245
|
+
def size
|
|
246
|
+
@mutex.synchronize { @registry.size }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Return all versions of an event (ADR-012 §6.2).
|
|
250
|
+
#
|
|
251
|
+
# @param event_name [String] Event name
|
|
252
|
+
# @return [Array<Hash>] [{ version: N, class: Klass }, ...] sorted by version
|
|
253
|
+
def all_versions(event_name)
|
|
254
|
+
entries = @mutex.synchronize { @registry[event_name.to_s]&.dup }
|
|
255
|
+
return [] if entries.nil? || entries.empty?
|
|
256
|
+
|
|
257
|
+
entries
|
|
258
|
+
.map { |klass| { version: klass.respond_to?(:version) ? klass.version : 1, class: klass } }
|
|
259
|
+
.sort_by { |h| h[:version] }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Return event names that have multiple versions (ADR-012 §6.2).
|
|
263
|
+
#
|
|
264
|
+
# @return [Array<String>]
|
|
265
|
+
def versioned_events
|
|
266
|
+
@mutex.synchronize do
|
|
267
|
+
@registry.select { |_name, entries| entries.size >= 2 }.keys
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Generate a documentation-friendly hash for every registered event class.
|
|
272
|
+
#
|
|
273
|
+
# @return [Array<Hash>] Each entry contains `:name`, `:class`, `:version`,
|
|
274
|
+
# `:severity`, and `:schema_keys` (absent when not applicable).
|
|
275
|
+
def to_documentation
|
|
276
|
+
event_classes.map do |klass|
|
|
277
|
+
{
|
|
278
|
+
name: klass.respond_to?(:event_name) ? klass.event_name : klass.name,
|
|
279
|
+
class: klass.name,
|
|
280
|
+
version: klass.respond_to?(:version) ? klass.version : nil,
|
|
281
|
+
severity: klass.respond_to?(:severity) ? klass.severity : nil,
|
|
282
|
+
schema_keys: extract_schema_keys(klass)
|
|
283
|
+
}.compact
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
private
|
|
288
|
+
|
|
289
|
+
# Extract schema key names from an event class.
|
|
290
|
+
#
|
|
291
|
+
# Uses `compiled_schema.key_map` when available, falling back gracefully.
|
|
292
|
+
#
|
|
293
|
+
# @param klass [Class] Event class
|
|
294
|
+
# @return [Array<String>, nil]
|
|
295
|
+
def extract_schema_keys(klass)
|
|
296
|
+
return nil unless klass.respond_to?(:compiled_schema)
|
|
297
|
+
|
|
298
|
+
schema = klass.compiled_schema
|
|
299
|
+
return nil unless schema.respond_to?(:key_map)
|
|
300
|
+
|
|
301
|
+
schema.key_map.keys.map(&:name)
|
|
302
|
+
rescue StandardError
|
|
303
|
+
nil
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -115,7 +115,7 @@ module E11y
|
|
|
115
115
|
|
|
116
116
|
# Handle OPEN state (fast fail).
|
|
117
117
|
def handle_open_circuit
|
|
118
|
-
|
|
118
|
+
E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "rejected")
|
|
119
119
|
|
|
120
120
|
raise CircuitOpenError, "Circuit breaker open for #{@adapter_name} " \
|
|
121
121
|
"(opened at #{@opened_at}, timeout: #{@timeout_seconds}s)"
|
|
@@ -137,12 +137,10 @@ module E11y
|
|
|
137
137
|
@failure_count = 0
|
|
138
138
|
@success_count += 1
|
|
139
139
|
end
|
|
140
|
-
|
|
141
|
-
increment_metric("e11y.circuit_breaker.success")
|
|
142
140
|
end
|
|
143
141
|
|
|
144
142
|
# Handle failed execution in CLOSED state.
|
|
145
|
-
def on_failure(
|
|
143
|
+
def on_failure(_error)
|
|
146
144
|
@mutex.synchronize do
|
|
147
145
|
@failure_count += 1
|
|
148
146
|
@last_failure_time = Time.now
|
|
@@ -150,8 +148,6 @@ module E11y
|
|
|
150
148
|
# Transition CLOSED → OPEN if threshold exceeded
|
|
151
149
|
transition_to_open if @failure_count >= @failure_threshold
|
|
152
150
|
end
|
|
153
|
-
|
|
154
|
-
increment_metric("e11y.circuit_breaker.failure", error: error.class.name)
|
|
155
151
|
end
|
|
156
152
|
|
|
157
153
|
# Handle successful execution in HALF_OPEN state.
|
|
@@ -162,18 +158,14 @@ module E11y
|
|
|
162
158
|
# Transition HALF_OPEN → CLOSED after enough successes
|
|
163
159
|
transition_to_closed if @success_count >= @half_open_attempts
|
|
164
160
|
end
|
|
165
|
-
|
|
166
|
-
increment_metric("e11y.circuit_breaker.half_open_success")
|
|
167
161
|
end
|
|
168
162
|
|
|
169
163
|
# Handle failed execution in HALF_OPEN state.
|
|
170
|
-
def on_half_open_failure(
|
|
164
|
+
def on_half_open_failure(_error)
|
|
171
165
|
@mutex.synchronize do
|
|
172
166
|
# Single failure in HALF_OPEN → back to OPEN
|
|
173
167
|
transition_to_open
|
|
174
168
|
end
|
|
175
|
-
|
|
176
|
-
increment_metric("e11y.circuit_breaker.half_open_failure", error: error.class.name)
|
|
177
169
|
end
|
|
178
170
|
|
|
179
171
|
# Transition to OPEN state.
|
|
@@ -183,7 +175,8 @@ module E11y
|
|
|
183
175
|
@failure_count = 0 # Reset for next cycle
|
|
184
176
|
@success_count = 0
|
|
185
177
|
|
|
186
|
-
|
|
178
|
+
E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "opened")
|
|
179
|
+
track_circuit_state_gauge
|
|
187
180
|
end
|
|
188
181
|
|
|
189
182
|
# Transition to HALF_OPEN state.
|
|
@@ -191,7 +184,8 @@ module E11y
|
|
|
191
184
|
@state = STATE_HALF_OPEN
|
|
192
185
|
@success_count = 0 # Reset success counter for testing
|
|
193
186
|
|
|
194
|
-
|
|
187
|
+
E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "half_opened")
|
|
188
|
+
track_circuit_state_gauge
|
|
195
189
|
end
|
|
196
190
|
|
|
197
191
|
# Transition to CLOSED state.
|
|
@@ -202,16 +196,20 @@ module E11y
|
|
|
202
196
|
@opened_at = nil
|
|
203
197
|
@last_failure_time = nil
|
|
204
198
|
|
|
205
|
-
|
|
199
|
+
E11y::Metrics.increment(:e11y_circuit_breaker_transitions_total, adapter: @adapter_name, event: "closed")
|
|
200
|
+
track_circuit_state_gauge
|
|
206
201
|
end
|
|
207
202
|
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
203
|
+
# Track circuit breaker state gauge via ReliabilityMonitor.
|
|
204
|
+
def track_circuit_state_gauge
|
|
205
|
+
return unless defined?(E11y::SelfMonitoring::ReliabilityMonitor)
|
|
206
|
+
|
|
207
|
+
E11y::SelfMonitoring::ReliabilityMonitor.track_circuit_state(
|
|
208
|
+
adapter_name: @adapter_name,
|
|
209
|
+
state: @state.to_s
|
|
210
|
+
)
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
E11y.logger&.warn("E11y CircuitBreaker gauge error: #{e.message}")
|
|
215
213
|
end
|
|
216
214
|
end
|
|
217
215
|
# rubocop:enable Metrics/ClassLength
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Reliability
|
|
5
|
+
module DLQ
|
|
6
|
+
# Abstract base class for Dead Letter Queue storage backends.
|
|
7
|
+
#
|
|
8
|
+
# Subclass this to implement a custom DLQ backend (file, Redis, database, etc.).
|
|
9
|
+
# All methods raise NotImplementedError by default except replay_batch (which
|
|
10
|
+
# delegates to replay).
|
|
11
|
+
#
|
|
12
|
+
# @see DLQ::FileAdapter for the file-based implementation
|
|
13
|
+
class Base
|
|
14
|
+
# Save a failed event to the DLQ.
|
|
15
|
+
#
|
|
16
|
+
# @param event_data [Hash] Event data
|
|
17
|
+
# @param metadata [Hash] Failure metadata
|
|
18
|
+
# @return [String] event ID
|
|
19
|
+
def save(event_data, metadata: {})
|
|
20
|
+
raise NotImplementedError, "#{self.class}#save is not implemented"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# List DLQ entries.
|
|
24
|
+
#
|
|
25
|
+
# @param limit [Integer]
|
|
26
|
+
# @param offset [Integer]
|
|
27
|
+
# @param filters [Hash]
|
|
28
|
+
# @return [Array<Hash>]
|
|
29
|
+
def list(limit: 100, offset: 0, filters: {})
|
|
30
|
+
raise NotImplementedError, "#{self.class}#list is not implemented"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Return DLQ statistics.
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash]
|
|
36
|
+
def stats
|
|
37
|
+
raise NotImplementedError, "#{self.class}#stats is not implemented"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Replay a single event.
|
|
41
|
+
#
|
|
42
|
+
# @param event_id [String]
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def replay(event_id)
|
|
45
|
+
raise NotImplementedError, "#{self.class}#replay is not implemented"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Replay a batch of events. Delegates to replay for each ID.
|
|
49
|
+
#
|
|
50
|
+
# @param event_ids [Array<String>]
|
|
51
|
+
# @return [Hash] { success_count: Integer, failure_count: Integer }
|
|
52
|
+
def replay_batch(event_ids)
|
|
53
|
+
success_count = 0
|
|
54
|
+
failure_count = 0
|
|
55
|
+
event_ids.each do |id|
|
|
56
|
+
replay(id) ? success_count += 1 : failure_count += 1
|
|
57
|
+
end
|
|
58
|
+
{ success_count: success_count, failure_count: failure_count }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Delete an entry from the DLQ.
|
|
62
|
+
#
|
|
63
|
+
# @param event_id [String]
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def delete(event_id)
|
|
66
|
+
raise NotImplementedError, "#{self.class}#delete is not implemented"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|