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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** Implemented
|
|
4
4
|
**Date:** January 12, 2026 (Updated: January 20, 2026)
|
|
5
|
-
**Covers:** UC-003 (
|
|
5
|
+
**Covers:** UC-003 (Event Metrics), UC-013 (High Cardinality Protection)
|
|
6
6
|
**Depends On:** ADR-001 (Core Architecture)
|
|
7
7
|
|
|
8
8
|
**Implementation Notes:** Refactored to "Rails Way" architecture (January 20, 2026) - see [IMPLEMENTATION_NOTES.md](./IMPLEMENTATION_NOTES.md#2026-01-20-metrics-architecture-refactoring---rails-way-)
|
|
@@ -13,14 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
1. [Context & Problem](#1-context--problem)
|
|
15
15
|
2. [Architecture Overview](#2-architecture-overview)
|
|
16
|
-
3. [
|
|
16
|
+
3. [Event Metrics](#3-event-metrics)
|
|
17
17
|
4. [Cardinality Protection](#4-cardinality-protection)
|
|
18
|
-
- 4.1. [
|
|
18
|
+
- 4.1. [Three-Layer Defense](#41-three-layer-defense)
|
|
19
19
|
- 4.2. [Layer 1: Universal Denylist](#42-layer-1-universal-denylist)
|
|
20
|
-
- 4.3. [Layer 2:
|
|
21
|
-
- 4.4. [Layer 3:
|
|
22
|
-
- 4.5. [
|
|
23
|
-
- 4.6. [Relabeling Rules](#46-relabeling-rules)
|
|
20
|
+
- 4.3. [Layer 2: Per-Metric Limits](#43-layer-2-per-metric-cardinality-limits)
|
|
21
|
+
- 4.4. [Layer 3: Dynamic Actions](#44-layer-3-dynamic-actions)
|
|
22
|
+
- 4.5. [Relabeling Rules](#45-relabeling-rules)
|
|
24
23
|
5. [Yabeda Integration](#5-yabeda-integration)
|
|
25
24
|
6. [Self-Monitoring](#6-self-monitoring)
|
|
26
25
|
7. [Configuration](#7-configuration)
|
|
@@ -142,7 +141,7 @@ end
|
|
|
142
141
|
# Adapter automatically:
|
|
143
142
|
# 1. Finds matching metrics from Registry
|
|
144
143
|
# 2. Extracts labels from event data
|
|
145
|
-
# 3. Applies 3-layer cardinality protection
|
|
144
|
+
# 3. Applies 3-layer cardinality protection (denylist, per-metric limits, dynamic actions)
|
|
146
145
|
# 4. Updates Yabeda metrics
|
|
147
146
|
```
|
|
148
147
|
|
|
@@ -178,21 +177,7 @@ end
|
|
|
178
177
|
|
|
179
178
|
### 0.5. Global Metrics via Registry
|
|
180
179
|
|
|
181
|
-
**
|
|
182
|
-
|
|
183
|
-
```ruby
|
|
184
|
-
# config/initializers/e11y.rb
|
|
185
|
-
E11y.configure do |config|
|
|
186
|
-
# Global metric for all order.* events
|
|
187
|
-
E11y::Metrics::Registry.instance.register(
|
|
188
|
-
type: :counter,
|
|
189
|
-
pattern: 'order.*', # Matches order.created, order.paid, etc.
|
|
190
|
-
name: :orders_total,
|
|
191
|
-
tags: [:currency, :status],
|
|
192
|
-
source: 'config/initializers/e11y.rb'
|
|
193
|
-
)
|
|
194
|
-
end
|
|
195
|
-
```
|
|
180
|
+
**Event-level metrics** — each event class defines its own metrics via `metrics do ... end` DSL. The Registry validates and registers them at boot time.
|
|
196
181
|
|
|
197
182
|
### 0.6. Key Benefits
|
|
198
183
|
|
|
@@ -236,8 +221,8 @@ end
|
|
|
236
221
|
> **⚠️ NOTE (C03 Resolution):** Yabeda is the **default metrics backend** for E11y. OpenTelemetry metrics are **optional** (see ADR-007). Choose ONE backend to avoid double overhead. See [CONFLICT-ANALYSIS.md C03](../researches/CONFLICT-ANALYSIS.md#c03-dual-metrics-collection-overhead) for details.
|
|
237
222
|
|
|
238
223
|
**Primary Goals:**
|
|
239
|
-
- ✅ Auto-create metrics from events (
|
|
240
|
-
- ✅ Prevent cardinality explosions (
|
|
224
|
+
- ✅ Auto-create metrics from events (event-level DSL)
|
|
225
|
+
- ✅ Prevent cardinality explosions (3-layer defense)
|
|
241
226
|
- ✅ Zero manual metric definitions
|
|
242
227
|
- ✅ Prometheus-friendly
|
|
243
228
|
- ✅ Cost-effective (<10k time series per metric)
|
|
@@ -308,7 +293,7 @@ graph TB
|
|
|
308
293
|
subgraph "Protection (3-Layer)"
|
|
309
294
|
L1[Layer 1: Denylist]
|
|
310
295
|
L2[Layer 2: Per-Metric Limits]
|
|
311
|
-
L3[Layer 3:
|
|
296
|
+
L3[Layer 3: Dynamic Actions]
|
|
312
297
|
end
|
|
313
298
|
|
|
314
299
|
EventClass -->|defines| MetricsDSL
|
|
@@ -355,8 +340,7 @@ sequenceDiagram
|
|
|
355
340
|
loop For each metric
|
|
356
341
|
Middleware->>Protection: extract_labels(event_data)
|
|
357
342
|
Protection->>Protection: Check denylist (order_id? ❌)
|
|
358
|
-
Protection->>Protection: Check
|
|
359
|
-
Protection->>Protection: Check cardinality (3 unique values ✅)
|
|
343
|
+
Protection->>Protection: Check cardinality (status: 3 unique values ✅)
|
|
360
344
|
Protection-->>Middleware: safe_labels: {status: 'paid'}
|
|
361
345
|
|
|
362
346
|
Middleware->>Yabeda: counter.increment(safe_labels)
|
|
@@ -370,7 +354,7 @@ sequenceDiagram
|
|
|
370
354
|
|
|
371
355
|
---
|
|
372
356
|
|
|
373
|
-
## 3.
|
|
357
|
+
## 3. Event Metrics
|
|
374
358
|
|
|
375
359
|
### 3.1. Pattern Matching
|
|
376
360
|
|
|
@@ -449,30 +433,13 @@ graph TB
|
|
|
449
433
|
|
|
450
434
|
**Configuration:**
|
|
451
435
|
|
|
436
|
+
|
|
452
437
|
```ruby
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
counter
|
|
457
|
-
|
|
458
|
-
comment: 'Total orders by status',
|
|
459
|
-
tags: [:status], # ← Extract from payload
|
|
460
|
-
unit: :count
|
|
461
|
-
|
|
462
|
-
# Histogram: measure values
|
|
463
|
-
histogram pattern: 'order.paid',
|
|
464
|
-
name: 'order_amount',
|
|
465
|
-
comment: 'Order payment amounts',
|
|
466
|
-
tags: [:payment_method],
|
|
467
|
-
unit: :dollars,
|
|
468
|
-
buckets: [10, 50, 100, 500, 1000, 5000]
|
|
469
|
-
|
|
470
|
-
# Gauge: current value
|
|
471
|
-
gauge pattern: 'buffer.*',
|
|
472
|
-
name: 'buffer_size',
|
|
473
|
-
comment: 'Current buffer size',
|
|
474
|
-
tags: [:buffer_type],
|
|
475
|
-
unit: :events
|
|
438
|
+
# Event-level metrics (implemented)
|
|
439
|
+
class Events::OrderPaid < E11y::Event::Base
|
|
440
|
+
metrics do
|
|
441
|
+
counter :orders_total, tags: [:status]
|
|
442
|
+
histogram :order_amount, value: :amount, tags: [:payment_method], buckets: [10, 50, 100, 500]
|
|
476
443
|
end
|
|
477
444
|
end
|
|
478
445
|
```
|
|
@@ -540,7 +507,7 @@ Layers are applied **sequentially** (not simultaneously):
|
|
|
540
507
|
|
|
541
508
|
1. **Layer 1 (Universal Denylist):** If label in denylist → DROP, stop processing
|
|
542
509
|
2. **Layer 2 (Per-Metric Limits):** Track unique values per label, drop if exceeded
|
|
543
|
-
3. **Layer 3 (
|
|
510
|
+
3. **Layer 3 (Dynamic Actions):** Configurable action on overflow (drop, alert, relabel)
|
|
544
511
|
|
|
545
512
|
**Example Flow:**
|
|
546
513
|
|
|
@@ -550,13 +517,12 @@ Label: user_id
|
|
|
550
517
|
|
|
551
518
|
Label: status
|
|
552
519
|
→ Layer 1: in FORBIDDEN_LABELS? ❌ No → continue
|
|
553
|
-
→ Layer 2:
|
|
520
|
+
→ Layer 2: cardinality < limit? ✅ Yes (3 values) → KEEP ✅
|
|
554
521
|
|
|
555
522
|
Label: custom_field
|
|
556
523
|
→ Layer 1: in FORBIDDEN_LABELS? ❌ No → continue
|
|
557
|
-
→ Layer 2:
|
|
558
|
-
→ Layer 3:
|
|
559
|
-
→ Layer 4: action=drop → DROP ❌
|
|
524
|
+
→ Layer 2: cardinality < limit? ❌ No (150 > 100) → continue
|
|
525
|
+
→ Layer 3: action=drop → DROP ❌
|
|
560
526
|
```
|
|
561
527
|
|
|
562
528
|
```mermaid
|
|
@@ -564,16 +530,13 @@ graph TB
|
|
|
564
530
|
Input[Event Labels] --> L1{Layer 1<br/>Denylist}
|
|
565
531
|
|
|
566
532
|
L1 -->|In denylist| Drop1[❌ Drop Label]
|
|
567
|
-
L1 -->|Not in denylist| L2{Layer 2<br/>
|
|
568
|
-
|
|
569
|
-
L2 -->|In allowlist| Keep[✅ Keep Label]
|
|
570
|
-
L2 -->|Not in allowlist| L3{Layer 3<br/>Cardinality Limit}
|
|
533
|
+
L1 -->|Not in denylist| L2{Layer 2<br/>Per-Metric Limit}
|
|
571
534
|
|
|
572
|
-
|
|
573
|
-
|
|
535
|
+
L2 -->|Under limit| Keep[✅ Keep Label]
|
|
536
|
+
L2 -->|Over limit| L3{Layer 3<br/>Dynamic Action}
|
|
574
537
|
|
|
575
|
-
|
|
576
|
-
|
|
538
|
+
L3 -->|drop| Drop2[❌ Drop Label]
|
|
539
|
+
L3 -->|alert| Alert[🚨 Alert + Drop]
|
|
577
540
|
|
|
578
541
|
Drop1 --> Log1[Log: denylist_hit]
|
|
579
542
|
Drop2 --> Log2[Log: cardinality_exceeded]
|
|
@@ -582,9 +545,8 @@ graph TB
|
|
|
582
545
|
Keep --> Metric[Export to Yabeda]
|
|
583
546
|
|
|
584
547
|
style L1 fill:#f8d7da
|
|
585
|
-
style L2 fill:#
|
|
586
|
-
style L3 fill:#
|
|
587
|
-
style L4 fill:#d1ecf1
|
|
548
|
+
style L2 fill:#fff3cd
|
|
549
|
+
style L3 fill:#d1ecf1
|
|
588
550
|
style Drop1 fill:#f8d7da
|
|
589
551
|
style Drop2 fill:#f8d7da
|
|
590
552
|
style Keep fill:#d4edda
|
|
@@ -673,53 +635,7 @@ graph LR
|
|
|
673
635
|
style File fill:#d4edda
|
|
674
636
|
```
|
|
675
637
|
|
|
676
|
-
### 4.3. Layer 2:
|
|
677
|
-
|
|
678
|
-
**Decision:** Pre-approved low-cardinality labels.
|
|
679
|
-
|
|
680
|
-
```ruby
|
|
681
|
-
module E11y
|
|
682
|
-
module Metrics
|
|
683
|
-
class CardinalityProtection
|
|
684
|
-
# Safe labels (low cardinality, always allowed)
|
|
685
|
-
SAFE_LABELS = [
|
|
686
|
-
# Status/state
|
|
687
|
-
:status, :state, :result, :outcome,
|
|
688
|
-
|
|
689
|
-
# Types/categories
|
|
690
|
-
:type, :kind, :category, :class_name,
|
|
691
|
-
|
|
692
|
-
# Methods/operations
|
|
693
|
-
:method, :action, :operation, :command,
|
|
694
|
-
|
|
695
|
-
# Environments
|
|
696
|
-
:env, :environment, :region, :zone, :datacenter,
|
|
697
|
-
|
|
698
|
-
# Services
|
|
699
|
-
:service, :component, :adapter, :backend,
|
|
700
|
-
|
|
701
|
-
# Severities
|
|
702
|
-
:severity, :level, :priority,
|
|
703
|
-
|
|
704
|
-
# HTTP
|
|
705
|
-
:http_method, :http_status, :http_status_class, # (200 → '2xx')
|
|
706
|
-
|
|
707
|
-
# Protocols
|
|
708
|
-
:protocol, :version,
|
|
709
|
-
|
|
710
|
-
# Success/failure
|
|
711
|
-
:success, :error_type, :error_class
|
|
712
|
-
].freeze
|
|
713
|
-
|
|
714
|
-
def in_allowlist?(label_name)
|
|
715
|
-
SAFE_LABELS.include?(label_name.to_sym)
|
|
716
|
-
end
|
|
717
|
-
end
|
|
718
|
-
end
|
|
719
|
-
end
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
### 4.4. Layer 3: Per-Metric Cardinality Limits
|
|
638
|
+
### 4.3. Layer 2: Per-Metric Cardinality Limits
|
|
723
639
|
|
|
724
640
|
**Decision:** Track unique values per label, enforce limits.
|
|
725
641
|
|
|
@@ -783,7 +699,7 @@ tracker.check_and_track('orders_total', :status, 'cancelled') # ❌ false (limi
|
|
|
783
699
|
tracker.check_and_track('orders_total', :status, 'paid') # ✅ true (seen before)
|
|
784
700
|
```
|
|
785
701
|
|
|
786
|
-
### 4.
|
|
702
|
+
### 4.4. Layer 3: Dynamic Actions
|
|
787
703
|
|
|
788
704
|
**Decision:** Configurable actions when limits exceeded.
|
|
789
705
|
|
|
@@ -822,8 +738,8 @@ Events::ApiCall.track(
|
|
|
822
738
|
customer_id: 'cust_12345' # ← 101st unique value, exceeds limit
|
|
823
739
|
)
|
|
824
740
|
|
|
825
|
-
# Layer
|
|
826
|
-
# Layer
|
|
741
|
+
# Layer 2: cardinality exceeded (100 limit)
|
|
742
|
+
# Layer 3: action=drop → customer_id dropped
|
|
827
743
|
|
|
828
744
|
# Result metric:
|
|
829
745
|
# api_calls_total{endpoint="/api/users"} 1
|
|
@@ -863,7 +779,7 @@ graph TB
|
|
|
863
779
|
|
|
864
780
|
**Note:** For v1.0, we keep it simple with just **drop** and **alert**. Advanced strategies (hash bucketing, aggregation) can be added in v1.1+ if needed.
|
|
865
781
|
|
|
866
|
-
### 4.
|
|
782
|
+
### 4.5. Relabeling Rules
|
|
867
783
|
|
|
868
784
|
**Decision:** Transform high-cardinality labels to low-cardinality.
|
|
869
785
|
|
|
@@ -937,21 +853,16 @@ end
|
|
|
937
853
|
|
|
938
854
|
### 5.2. Metric Registration
|
|
939
855
|
|
|
856
|
+
|
|
940
857
|
```ruby
|
|
941
|
-
#
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
counter
|
|
945
|
-
name: :orders_total,
|
|
946
|
-
comment: 'Total orders',
|
|
947
|
-
tags: [:status]
|
|
858
|
+
# Event-level metrics (implemented)
|
|
859
|
+
class Events::OrderCreated < E11y::Event::Base
|
|
860
|
+
metrics do
|
|
861
|
+
counter :orders_total, tags: [:status]
|
|
948
862
|
end
|
|
949
863
|
end
|
|
950
864
|
|
|
951
|
-
#
|
|
952
|
-
Yabeda.e11y.counter :orders_total,
|
|
953
|
-
tags: [:status],
|
|
954
|
-
comment: 'Total orders'
|
|
865
|
+
# Yabeda adapter auto-registers from Registry when events are loaded
|
|
955
866
|
```
|
|
956
867
|
|
|
957
868
|
### 5.3. Metric Updates
|
|
@@ -1052,97 +963,17 @@ end
|
|
|
1052
963
|
```ruby
|
|
1053
964
|
# config/initializers/e11y.rb
|
|
1054
965
|
E11y.configure do |config|
|
|
1055
|
-
config.metrics
|
|
1056
|
-
# Enable metrics
|
|
1057
|
-
enabled true
|
|
1058
|
-
|
|
1059
|
-
# Yabeda integration
|
|
1060
|
-
yabeda_integration true
|
|
1061
|
-
|
|
1062
|
-
# ===== Pattern-Based Metrics =====
|
|
1063
|
-
|
|
1064
|
-
# Counter: count all orders
|
|
1065
|
-
counter pattern: 'order.*',
|
|
1066
|
-
name: :orders_total,
|
|
1067
|
-
comment: 'Total orders by status and payment method',
|
|
1068
|
-
tags: [:status, :payment_method],
|
|
1069
|
-
unit: :count
|
|
1070
|
-
|
|
1071
|
-
# Histogram: order amounts
|
|
1072
|
-
histogram pattern: 'order.paid',
|
|
1073
|
-
name: :order_amount,
|
|
1074
|
-
comment: 'Order payment amounts',
|
|
1075
|
-
tags: [:payment_method, :currency],
|
|
1076
|
-
value_field: :amount, # Extract from payload
|
|
1077
|
-
unit: :dollars,
|
|
1078
|
-
buckets: [10, 50, 100, 500, 1000, 5000, 10000]
|
|
1079
|
-
|
|
1080
|
-
# Histogram: API response times
|
|
1081
|
-
histogram pattern: 'api.*',
|
|
1082
|
-
name: :api_duration_seconds,
|
|
1083
|
-
comment: 'API call durations',
|
|
1084
|
-
tags: [:endpoint, :http_status_class],
|
|
1085
|
-
value_field: :duration,
|
|
1086
|
-
unit: :seconds,
|
|
1087
|
-
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5]
|
|
1088
|
-
|
|
1089
|
-
# ===== Cardinality Protection =====
|
|
1090
|
-
|
|
1091
|
-
cardinality_protection do
|
|
1092
|
-
# Default limit per label
|
|
1093
|
-
default_cardinality_limit 100
|
|
1094
|
-
|
|
1095
|
-
# Per-metric limits
|
|
1096
|
-
per_metric do
|
|
1097
|
-
metric :orders_total, label: :status, limit: 10
|
|
1098
|
-
metric :orders_total, label: :payment_method, limit: 20
|
|
1099
|
-
metric :api_duration_seconds, label: :endpoint, limit: 500
|
|
1100
|
-
end
|
|
1101
|
-
|
|
1102
|
-
# Action on excess
|
|
1103
|
-
action_on_excess :drop # :drop or :alert
|
|
1104
|
-
|
|
1105
|
-
# Denylist (in addition to universal)
|
|
1106
|
-
forbidden_labels [
|
|
1107
|
-
:customer_id,
|
|
1108
|
-
:internal_ref
|
|
1109
|
-
]
|
|
1110
|
-
|
|
1111
|
-
# Allowlist (in addition to safe list)
|
|
1112
|
-
allowed_labels [
|
|
1113
|
-
:subscription_tier,
|
|
1114
|
-
:user_role
|
|
1115
|
-
]
|
|
1116
|
-
|
|
1117
|
-
# Relabeling rules
|
|
1118
|
-
relabel :http_status do |value|
|
|
1119
|
-
"#{value.to_i / 100}xx"
|
|
1120
|
-
end
|
|
1121
|
-
|
|
1122
|
-
relabel :path do |value|
|
|
1123
|
-
value.gsub(/\/\d+/, '/:id')
|
|
1124
|
-
end
|
|
1125
|
-
|
|
1126
|
-
# Monitoring
|
|
1127
|
-
monitoring do
|
|
1128
|
-
alert_on_new_label true
|
|
1129
|
-
alert_threshold 80 # Alert at 80% of limit
|
|
1130
|
-
|
|
1131
|
-
on_alert do |metric_name, label_name, cardinality, limit|
|
|
1132
|
-
Rails.logger.warn "Cardinality alert: #{metric_name}.#{label_name} = #{cardinality}/#{limit}"
|
|
1133
|
-
end
|
|
1134
|
-
end
|
|
1135
|
-
end
|
|
1136
|
-
|
|
1137
|
-
# ===== Advanced Features =====
|
|
1138
|
-
|
|
1139
|
-
# Exemplars (sample trace IDs for metric values)
|
|
1140
|
-
exemplars do
|
|
1141
|
-
enabled true
|
|
1142
|
-
max_per_bucket 1 # 1 trace_id per histogram bucket
|
|
1143
|
-
end
|
|
1144
|
-
end
|
|
966
|
+
config.adapters[:metrics] = E11y::Adapters::Yabeda.new
|
|
1145
967
|
end
|
|
968
|
+
|
|
969
|
+
# Define metrics in event classes:
|
|
970
|
+
# class Events::OrderPaid < E11y::Event::Base
|
|
971
|
+
# metrics do
|
|
972
|
+
# counter :orders_total, tags: [:status, :payment_method]
|
|
973
|
+
# histogram :order_amount, value: :amount, tags: [:payment_method, :currency],
|
|
974
|
+
# buckets: [10, 50, 100, 500, 1000, 5000]
|
|
975
|
+
# end
|
|
976
|
+
# end
|
|
1146
977
|
```
|
|
1147
978
|
|
|
1148
979
|
---
|
|
@@ -1238,7 +1069,7 @@ RSpec.describe E11y::Metrics::CardinalityProtection do
|
|
|
1238
1069
|
end
|
|
1239
1070
|
end
|
|
1240
1071
|
|
|
1241
|
-
describe 'Layer
|
|
1072
|
+
describe 'Layer 2: Cardinality Limits' do
|
|
1242
1073
|
it 'enforces per-metric limits' do
|
|
1243
1074
|
protection = CardinalityProtection.new(
|
|
1244
1075
|
limits: { orders_total: { status: 3 } }
|
|
@@ -1269,7 +1100,7 @@ end
|
|
|
1269
1100
|
|----------|-----|-----|-----------|
|
|
1270
1101
|
| **Auto-metrics** | Zero boilerplate | Less control | DX > control |
|
|
1271
1102
|
| **Pattern matching** | Flexible | Slower than exact | Flexibility matters |
|
|
1272
|
-
| **
|
|
1103
|
+
| **3-layer defense** | Robust | Complex | Safety critical |
|
|
1273
1104
|
| **Hash bucketing** | Preserves some signal | Loss of precision | Better than drop |
|
|
1274
1105
|
| **Yabeda dependency** | Battle-tested | External dep | Standard in Ruby |
|
|
1275
1106
|
|
|
@@ -1310,7 +1141,7 @@ Loki/Sentry/File:
|
|
|
1310
1141
|
|
|
1311
1142
|
---
|
|
1312
1143
|
|
|
1313
|
-
### Q2: Are Layers 1
|
|
1144
|
+
### Q2: Are Layers 1–3 applied simultaneously or sequentially?
|
|
1314
1145
|
|
|
1315
1146
|
**A: Sequentially (waterfall), not simultaneously.**
|
|
1316
1147
|
|
|
@@ -1321,16 +1152,12 @@ Processing order:
|
|
|
1321
1152
|
↓ If in denylist → DROP, stop
|
|
1322
1153
|
↓ If not in denylist → continue to Layer 2
|
|
1323
1154
|
|
|
1324
|
-
2. Layer 2 (
|
|
1325
|
-
↓ If in allowlist → KEEP, skip Layer 3-4
|
|
1326
|
-
↓ If not in allowlist → continue to Layer 3
|
|
1327
|
-
|
|
1328
|
-
3. Layer 3 (Cardinality Limit)
|
|
1155
|
+
2. Layer 2 (Per-Metric Limit)
|
|
1329
1156
|
↓ If under limit → KEEP, stop
|
|
1330
|
-
↓ If over limit → continue to Layer
|
|
1157
|
+
↓ If over limit → continue to Layer 3
|
|
1331
1158
|
|
|
1332
|
-
|
|
1333
|
-
↓ Apply configured action:
|
|
1159
|
+
3. Layer 3 (Dynamic Action)
|
|
1160
|
+
↓ Apply configured action: drop/alert/relabel
|
|
1334
1161
|
```
|
|
1335
1162
|
|
|
1336
1163
|
**Example:**
|
|
@@ -1343,15 +1170,14 @@ Processing order:
|
|
|
1343
1170
|
|
|
1344
1171
|
# status:
|
|
1345
1172
|
# Layer 1: not in FORBIDDEN_LABELS → continue
|
|
1346
|
-
# Layer 2:
|
|
1173
|
+
# Layer 2: cardinality under limit → ✅ KEEP
|
|
1347
1174
|
|
|
1348
1175
|
# tier:
|
|
1349
1176
|
# Layer 1: not in FORBIDDEN_LABELS → continue
|
|
1350
|
-
# Layer 2:
|
|
1351
|
-
# Layer 3:
|
|
1352
|
-
# Layer 4: action=hash → ✅ KEEP as "bucket_7"
|
|
1177
|
+
# Layer 2: cardinality = 150 > limit (100) → continue
|
|
1178
|
+
# Layer 3: action=drop → ❌ DROP
|
|
1353
1179
|
|
|
1354
|
-
# Result: { status: 'paid'
|
|
1180
|
+
# Result: { status: 'paid' }
|
|
1355
1181
|
```
|
|
1356
1182
|
|
|
1357
1183
|
---
|